diff --git a/cecli/args.py b/cecli/args.py index 6d26d53bd13..561f6911cbd 100644 --- a/cecli/args.py +++ b/cecli/args.py @@ -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( diff --git a/cecli/args_formatter.py b/cecli/args_formatter.py index 01b9bc94094..532a3481a32 100644 --- a/cecli/args_formatter.py +++ b/cecli/args_formatter.py @@ -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): diff --git a/cecli/coders/agent_coder.py b/cecli/coders/agent_coder.py index f523adbe4e1..d7261ccc63a 100644 --- a/cecli/coders/agent_coder.py +++ b/cecli/coders/agent_coder.py @@ -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 @@ -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): """ diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index c77c0e6c202..0e605acab72 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -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 @@ -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 diff --git a/cecli/main.py b/cecli/main.py index 2fea8b4946a..ba77c2a5c29 100644 --- a/cecli/main.py +++ b/cecli/main.py @@ -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, @@ -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) diff --git a/cecli/utils.py b/cecli/utils.py index aac9b20b597..1f9747a2306 100644 --- a/cecli/utils.py +++ b/cecli/utils.py @@ -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(): diff --git a/cecli/website/HISTORY.md b/cecli/website/HISTORY.md new file mode 100644 index 00000000000..cc49e64b313 --- /dev/null +++ b/cecli/website/HISTORY.md @@ -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 diff --git a/tests/test_conversation_system.py b/tests/test_conversation_system.py index 7f0cfe3d34d..f23753518c0 100644 --- a/tests/test_conversation_system.py +++ b/tests/test_conversation_system.py @@ -6,10 +6,10 @@ from cecli.helpers.conversation import ( BaseMessage, + ConversationChunks, ConversationFiles, ConversationManager, MessageTag, - initialize_conversation_system, ) from cecli.io import InputOutput @@ -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() @@ -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( @@ -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( @@ -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( @@ -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()