Skip to content
Draft
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
10 changes: 10 additions & 0 deletions cecli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,15 @@ 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 +1132,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
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