Skip to content
Closed
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
19 changes: 19 additions & 0 deletions cecli/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,25 @@ def get_parser(default_config_files, git_root):
default=False,
)

##########
group = parser.add_argument_group("Workspace settings")
# Custom handling for workspace-paths environment variable
workspace_paths_env = os.environ.get("CECLI_WORKSPACE_PATHS")
if workspace_paths_env:
# Split by colon or semicolon for path separation
workspace_paths_default = [
p.strip() for p in workspace_paths_env.replace(";", ":").split(":") if p.strip()
]
else:
workspace_paths_default = []
group.add_argument(
"--workspace-paths",
action="append",
metavar="WORKSPACE_PATH",
help="Specify additional workspace directories (can be used multiple times)",
default=workspace_paths_default,
)

##########
group = parser.add_argument_group("Security Settings")
group.add_argument(
Expand Down
3 changes: 3 additions & 0 deletions cecli/args_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ def _format_text(self, text):
# config file. Keys for all APIs can be stored in a .env file
# https://cecli.dev/docs/config/dotenv.html

# workspace-paths:
# - /path/to/shared/workspace
# - another/workspace
"""

def _format_action(self, action):
Expand Down
52 changes: 49 additions & 3 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class AgentCoder(Coder):
hashlines = True

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.recently_removed = {}
self.tool_usage_history = []
self.tool_usage_retries = 20
Expand Down Expand Up @@ -91,12 +92,57 @@ def __init__(self, *args, **kwargs):
self.skip_cli_confirmations = False
self.agent_finished = False
self.agent_config = self._get_agent_config()
self._setup_agent()
ToolRegistry.build_registry(agent_config=self.agent_config)
super().__init__(*args, **kwargs)

def _setup_agent(self):
os.makedirs(".cecli/workspace", exist_ok=True)
from cecli.utils import resolve_workspace_paths

self.resolved_workspace_paths = resolve_workspace_paths(
self.workspace_paths, self.repo.root if self.repo else None
)

def get_workspace_directory(self, preferred_name=None):
"""Get an appropriate workspace directory for temporary files.
Args:
preferred_name: Preferred name for the workspace subdirectory
Returns:
Path to a workspace directory
"""
from pathlib import Path

# If we have resolved workspace paths, try to use the first available one
if hasattr(self, "resolved_workspace_paths") and self.resolved_workspace_paths:
for workspace_path in self.resolved_workspace_paths:
try:
# Use this workspace path if it exists or its parent exists
if workspace_path.exists() or workspace_path.parent.exists():
if preferred_name:
workspace_dir = workspace_path / preferred_name
else:
workspace_dir = workspace_path
workspace_dir.mkdir(parents=True, exist_ok=True)
return workspace_dir
except Exception:
continue

# Fall back to default behavior
git_root = self.repo.root if self.repo else None
if git_root:
default_workspace = Path(git_root) / ".cecli" / "workspace"
else:
default_workspace = Path(".cecli") / "workspace"

if preferred_name:
res = default_workspace / preferred_name
else:
res = default_workspace

res.mkdir(parents=True, exist_ok=True)
return res

def local_agent_folder(self, path):
workspace_dir = self.get_workspace_directory()
return os.path.join(workspace_dir, path)

def _get_agent_config(self):
"""
Expand Down
2 changes: 2 additions & 0 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ def __init__(
repomap_in_memory=False,
linear_output=False,
security_config=None,
workspace_paths=None,
uuid="",
):
# initialize from args.map_cache_dir
Expand All @@ -342,6 +343,7 @@ def __init__(

self.auto_copy_context = auto_copy_context
self.security_config = security_config or {}
self.workspace_paths = workspace_paths
self.auto_accept_architect = auto_accept_architect

self.ignore_mentions = ignore_mentions
Expand Down
Binary file not shown.
6 changes: 6 additions & 0 deletions cecli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,11 @@ def apply_model_overrides(model_name):
f" {', '.join(loaded_hooks)}"
)

# Initialize workspace paths configuration
workspace_paths = args.workspace_paths if hasattr(args, "workspace_paths") and args.workspace_paths else []
if args.verbose and workspace_paths:
io.tool_output(f"Additional workspace paths configured: {workspace_paths}")

coder = await Coder.create(
main_model=main_model,
edit_format=args.edit_format,
Expand Down Expand Up @@ -1123,6 +1128,7 @@ def apply_model_overrides(model_name):
repomap_in_memory=args.map_memory_cache,
linear_output=args.linear_output,
security_config=args.security_config,
workspace_paths=workspace_paths,
)
if args.show_model_warnings and not suppress_pre_init:
problem = await models.sanity_check_models(pre_init_io, main_model)
Expand Down
41 changes: 41 additions & 0 deletions cecli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,47 @@ def safe_abs_path(res):
return str(res)


def resolve_workspace_paths(workspace_paths, git_root=None, default_workspace=".cecli/workspace"):
"""
Resolve workspace paths, including default and additional paths.
Args:
workspace_paths: List of additional workspace paths
git_root: Git root directory for relative path resolution
default_workspace: Default workspace directory name
Returns:
List of resolved workspace paths
"""
from pathlib import Path

resolved_paths = []

# Always include the default workspace path
if git_root:
default_path = Path(git_root) / default_workspace
else:
default_path = Path(default_workspace)
resolved_paths.append(default_path.resolve())

# Add additional workspace paths
for path in workspace_paths or []:
if not path:
continue
try:
if Path(path).is_absolute():
resolved_path = Path(path).expanduser().resolve()
elif git_root:
resolved_path = (Path(git_root) / path).expanduser().resolve()
else:
resolved_path = Path(path).expanduser().resolve()

if resolved_path not in resolved_paths:
resolved_paths.append(resolved_path)
except Exception:
continue

return resolved_paths


def format_content(role, content):
formatted_lines = []
for line in content.splitlines():
Expand Down
34 changes: 34 additions & 0 deletions cecli/website/HISTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## v0.XX.X (Upcoming Release)

### Features & Improvements
- **Multi-workspace Support**: Added `--workspace-paths` argument to specify multiple workspace directories
```bash
# CLI usage with multiple directories
cecli --workspace-paths /path/to/project1 --workspace-paths /path/to/project2

# Environment variable
export CECLI_WORKSPACE_PATHS="/path/to/project1:/path/to/project2"
cecli

# YAML configuration (.cecli.conf.yml)
workspace-paths:
- /path/to/project1
- /path/to/project2
```

Supports:
- Cross-repository operations
- Microservices architecture support
- Multiple component directories
- Relative and absolute paths
- Environment variable configuration

Resolution order: CLI arguments > Environment variable > YAML config > Current directory

### Developer Experience
- Extended skills framework to support multiple workspace contexts
- Various bug fixes and stability improvements

### Documentation
- Added comprehensive examples for multi-workspace configuration
- Updated CLI help and configuration templates
5 changes: 1 addition & 4 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
[pytest]
norecursedirs = tmp.* build benchmark _site OLD
addopts = -p no:warnings
addopts = -p no:warnings -p pytest_asyncio -p pytest_mock
asyncio_mode = auto
testpaths =
tests/basic
Expand All @@ -13,4 +11,3 @@ testpaths =

env =
CECLI_TUI=false

Binary file not shown.
Binary file not shown.
12 changes: 6 additions & 6 deletions tests/test_conversation_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

from cecli.helpers.conversation import (
BaseMessage,
ConversationChunks,
ConversationFiles,
ConversationManager,
MessageTag,
initialize_conversation_system,
)
from cecli.io import InputOutput

Expand Down Expand Up @@ -123,7 +123,7 @@ def setup(self):
self.test_coder = TestCoder()

# Initialize conversation system
initialize_conversation_system(self.test_coder)
ConversationChunks.initialize_conversation_system(self.test_coder)
yield
ConversationManager.reset()

Expand Down Expand Up @@ -487,7 +487,7 @@ def test_coder_properties(self):
coder = TestCoder()

# Initialize conversation system
initialize_conversation_system(coder)
ConversationChunks.initialize_conversation_system(coder)

# Add messages with different tags
ConversationManager.add_message(
Expand Down Expand Up @@ -523,7 +523,7 @@ def test_cache_control_headers(self):
# Create a test coder with add_cache_headers = False (default)
coder_false = TestCoder()
coder_false.add_cache_headers = False
initialize_conversation_system(coder_false)
ConversationChunks.initialize_conversation_system(coder_false)

# Add some messages
ConversationManager.add_message(
Expand Down Expand Up @@ -560,7 +560,7 @@ def test_cache_control_headers(self):

coder_true = TestCoder()
coder_true.add_cache_headers = True
initialize_conversation_system(coder_true)
ConversationChunks.initialize_conversation_system(coder_true)

# Add the same messages
ConversationManager.add_message(
Expand Down Expand Up @@ -631,7 +631,7 @@ def setup(self):
self.test_coder = TestCoder()

# Initialize conversation system
initialize_conversation_system(self.test_coder)
ConversationChunks.initialize_conversation_system(self.test_coder)
yield
ConversationFiles.reset()

Expand Down
Loading