From f252f935ee0ad47f44c6d0cc087347ab569b8d45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 05:59:39 +0000 Subject: [PATCH 01/15] Initial plan From a7440541ee22c632052a3389d7c341b9ffb3597c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:05:37 +0000 Subject: [PATCH 02/15] feat: add interactive init installer Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/95572185-7ffc-40bd-9def-cb7aa8ab330a Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- .../cli/commands/init_cmd.py | 201 ++++++++++++++---- semantic_code_intelligence/tests/test_cli.py | 24 +++ 2 files changed, 185 insertions(+), 40 deletions(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index d582bd8..a4f1ad7 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -10,10 +10,13 @@ from semantic_code_intelligence.config.settings import ( AppConfig, init_project, + load_config, save_config, ) from semantic_code_intelligence.embeddings.model_registry import ( CLI_PROFILE_CHOICES, + MODEL_PROFILES, + ModelProfile, recommend_profile_for_ram, resolve_profile, ) @@ -30,6 +33,9 @@ print_success, print_warning, ) +from rich.console import Console +from rich.panel import Panel +from rich.table import Table logger = get_logger("cli.init") @@ -102,8 +108,14 @@ def _generate_vscode_mcp_config(root: Path) -> bool: "Size aliases (small/base/large) and named aliases (default/quality/code) are supported." ), ) +@click.option( + "--interactive/--no-interactive", + "interactive", + default=False, + help="Launch the interactive installer to choose the embedding model and batch size.", +) @click.pass_context -def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool, profile_name: str | None) -> None: +def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool, profile_name: str | None, interactive: bool) -> None: """Initialize a project for semantic code indexing. Creates a .codexa/ directory with default configuration and an empty index. @@ -119,7 +131,8 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool # Check if already initialized config_dir = AppConfig.config_dir(root) - if config_dir.exists(): + config_path = AppConfig.config_path(root) + if config_dir.exists() and not interactive: print_info(f"Project already initialized at {root}") print_info(f"Config directory: {config_dir}") # Still allow --vscode and --index on existing projects @@ -133,11 +146,16 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool return try: - config, config_path = init_project(root) - print_success(f"Initialized project at {root}") - print_info(f"Config file: {config_path}") - print_info(f"Index directory: {AppConfig.index_dir(root)}") - logger.debug("Default config: %s", config.model_dump()) + if config_dir.exists(): + config = load_config(root) + print_info(f"Project already initialized at {root}") + print_info(f"Launching interactive installer to update configuration.") + else: + config, config_path = init_project(root) + print_success(f"Initialized project at {root}") + print_info(f"Config file: {config_path}") + print_info(f"Index directory: {AppConfig.index_dir(root)}") + logger.debug("Default config: %s", config.model_dump()) except OSError as e: print_error(f"Failed to initialize project: {e}") ctx.exit(1) @@ -149,48 +167,63 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool available_memory / BYTES_PER_GB if available_memory is not None else None ) - # Apply model profile (explicit or RAM-auto-detected) - profile = None + recommended_profile = None if profile_name: - profile = resolve_profile(profile_name) + recommended_profile = resolve_profile(profile_name) elif available_gb is not None: - profile = recommend_profile_for_ram(available_gb) - print_info(f"Detected {available_gb:.1f} GB available RAM → using '{profile.name}' profile ({profile.label})") - - profile_changed = False - if profile: - if config.embedding.model_name != profile.model_name: - config.embedding.model_name = profile.model_name - profile_changed = True - print_success(f"Model profile: {profile.label} → {profile.model_name}") - print_info(f" {profile.description}") + recommended_profile = recommend_profile_for_ram(available_gb) recommended_batch_size = recommend_batch_size(available_memory, logical_cpu_count) - batch_changed = recommended_batch_size != config.embedding.batch_size - if batch_changed: - config.embedding.batch_size = recommended_batch_size - resource_parts: list[str] = [] - if available_gb is not None: - resource_parts.append(f"{available_gb:.1f} GB RAM") - if logical_cpu_count is not None: - core_label = "CPU core" if logical_cpu_count == 1 else "CPU cores" - resource_parts.append(f"{logical_cpu_count} {core_label}") - - batch_message_prefix = ( - f"Embedding batch size {'updated' if batch_changed else 'kept'} " - f"at {config.embedding.batch_size}" - ) - if resource_parts: - print_info( - f"{batch_message_prefix} (based on {', '.join(resource_parts)})" + if interactive: + profile_changed, batch_changed = _run_interactive_installer( + config=config, + available_gb=available_gb, + cpu_count=logical_cpu_count, + default_profile=recommended_profile or MODEL_PROFILES["balanced"], + recommended_batch_size=recommended_batch_size, ) + should_save = profile_changed or batch_changed else: - print_info( - f"{batch_message_prefix} (using default recommendation)" + # Apply model profile (explicit or RAM-auto-detected) + profile = recommended_profile + profile_changed = False + if profile: + if profile_name is None and available_gb is not None: + print_info(f"Detected {available_gb:.1f} GB available RAM → using '{profile.name}' profile ({profile.label})") + + if config.embedding.model_name != profile.model_name: + config.embedding.model_name = profile.model_name + profile_changed = True + print_success(f"Model profile: {profile.label} → {profile.model_name}") + print_info(f" {profile.description}") + + batch_changed = recommended_batch_size != config.embedding.batch_size + if batch_changed: + config.embedding.batch_size = recommended_batch_size + + resource_parts: list[str] = [] + if available_gb is not None: + resource_parts.append(f"{available_gb:.1f} GB RAM") + if logical_cpu_count is not None: + core_label = "CPU core" if logical_cpu_count == 1 else "CPU cores" + resource_parts.append(f"{logical_cpu_count} {core_label}") + + batch_message_prefix = ( + f"Embedding batch size {'updated' if batch_changed else 'kept'} " + f"at {config.embedding.batch_size}" ) + if resource_parts: + print_info( + f"{batch_message_prefix} (based on {', '.join(resource_parts)})" + ) + else: + print_info( + f"{batch_message_prefix} (using default recommendation)" + ) + + should_save = profile_changed or batch_changed - should_save = profile_changed or batch_changed if should_save: save_config(config, root) @@ -210,6 +243,94 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool print_info(" .codexaignore — Exclude secrets or generated files from indexing") +def _run_interactive_installer( + config: AppConfig, + available_gb: float | None, + cpu_count: int | None, + default_profile: ModelProfile, + recommended_batch_size: int, +) -> tuple[bool, bool]: + """Launch a text-based interactive installer for model and batch settings.""" + console = Console() + console.print() + console.print(Panel.fit("[bold cyan]CodexA Interactive Installer[/bold cyan]\nConfigure embedding defaults for your project.", border_style="cyan")) + + # Resource summary and suggestions + resource_lines: list[str] = [] + if available_gb is not None: + resource_lines.append(f"[green]{available_gb:.1f} GB[/green] available RAM detected") + if cpu_count is not None: + resource_lines.append(f"[green]{cpu_count} CPU cores[/green] detected") + if resource_lines: + console.print(" • ".join(resource_lines)) + console.print(f"Suggested profile: [bold]{default_profile.label}[/bold]") + console.print(f"Suggested batch size: [bold]{recommended_batch_size}[/bold]") + else: + console.print("System resources could not be detected; keeping safe defaults.") + + # Show model options + table = Table(title="Embedding Profiles", show_lines=True) + table.add_column("Key", justify="center", style="cyan", no_wrap=True) + table.add_column("Label") + table.add_column("Model") + table.add_column("Description") + table.add_column("Min RAM (GB)", justify="right") + for key in ["fast", "balanced", "precise"]: + profile = MODEL_PROFILES[key] + table.add_row( + profile.name, + profile.label, + profile.model_name, + profile.description, + f"{profile.min_ram_gb:.1f}", + ) + console.print(table) + + chosen_profile_key = click.prompt( + "Select embedding profile", + type=click.Choice(["fast", "balanced", "precise"], case_sensitive=False), + default=default_profile.name, + show_choices=False, + ) + chosen_profile = resolve_profile(chosen_profile_key) or default_profile + + profile_changed = False + if config.embedding.model_name != chosen_profile.model_name: + config.embedding.model_name = chosen_profile.model_name + profile_changed = True + + console.print() + console.print( + Panel.fit( + f"[bold]Batch size[/bold] controls how many chunks are embedded at once.\n" + f"Recommended: [cyan]{recommended_batch_size}[/cyan] (based on detection).", + border_style="cyan", + ) + ) + + batch_input = click.prompt( + "Embedding batch size", + default=recommended_batch_size, + type=int, + show_default=True, + ) + if batch_input < 1: + batch_input = 1 + batch_changed = batch_input != config.embedding.batch_size + config.embedding.batch_size = batch_input + + console.print() + console.print( + Panel.fit( + f"Using profile [green]{chosen_profile.label}[/green] ({chosen_profile.model_name}) " + f"with batch size [green]{config.embedding.batch_size}[/green].", + border_style="green", + ) + ) + + return profile_changed, batch_changed + + def _run_index(root: Path) -> None: """Run indexing as part of init.""" from semantic_code_intelligence.services.indexing_service import index_project diff --git a/semantic_code_intelligence/tests/test_cli.py b/semantic_code_intelligence/tests/test_cli.py index 3b24304..c53fe71 100644 --- a/semantic_code_intelligence/tests/test_cli.py +++ b/semantic_code_intelligence/tests/test_cli.py @@ -110,6 +110,30 @@ def test_init_saves_recommended_batch_size(self, runner: CliRunner, tmp_path: Pa # Profile for ~3GB RAM should be precise according to registry thresholds assert config["embedding"]["model_name"] == "jinaai/jina-embeddings-v2-base-code" + def test_init_interactive_installer_allows_selection(self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + # Provide stable detection so defaults are deterministic + monkeypatch.setattr( + "semantic_code_intelligence.cli.commands.init_cmd._get_available_memory_bytes", + lambda: 5 * BYTES_PER_GB, + ) + monkeypatch.setattr( + "semantic_code_intelligence.cli.commands.init_cmd._get_cpu_count", + lambda: 4, + ) + + result = runner.invoke( + cli, + ["init", str(tmp_path), "--interactive"], + input="fast\n24\n", + ) + assert result.exit_code == 0 + output = result.output.lower() + assert "interactive installer" in output + + config = json.loads((tmp_path / ".codexa" / "config.json").read_text(encoding="utf-8")) + assert config["embedding"]["model_name"] == MODEL_PROFILES["fast"].model_name + assert config["embedding"]["batch_size"] == 24 + class TestIndexCommand: """Tests for the index command.""" From 3f51c1195c29a25054ec1d386097ef3919c65a79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:06:22 +0000 Subject: [PATCH 03/15] chore: address review feedback Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/95572185-7ffc-40bd-9def-cb7aa8ab330a Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/init_cmd.py | 9 +++++---- semantic_code_intelligence/tests/test_cli.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index a4f1ad7..99ff68c 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -292,7 +292,10 @@ def _run_interactive_installer( default=default_profile.name, show_choices=False, ) - chosen_profile = resolve_profile(chosen_profile_key) or default_profile + chosen_profile = resolve_profile(chosen_profile_key) + if chosen_profile is None: + # Should not happen due to click.Choice guard, but keep a safe fallback. + chosen_profile = default_profile profile_changed = False if config.embedding.model_name != chosen_profile.model_name: @@ -311,11 +314,9 @@ def _run_interactive_installer( batch_input = click.prompt( "Embedding batch size", default=recommended_batch_size, - type=int, + type=click.IntRange(1, None), show_default=True, ) - if batch_input < 1: - batch_input = 1 batch_changed = batch_input != config.embedding.batch_size config.embedding.batch_size = batch_input diff --git a/semantic_code_intelligence/tests/test_cli.py b/semantic_code_intelligence/tests/test_cli.py index c53fe71..7043fc3 100644 --- a/semantic_code_intelligence/tests/test_cli.py +++ b/semantic_code_intelligence/tests/test_cli.py @@ -110,7 +110,7 @@ def test_init_saves_recommended_batch_size(self, runner: CliRunner, tmp_path: Pa # Profile for ~3GB RAM should be precise according to registry thresholds assert config["embedding"]["model_name"] == "jinaai/jina-embeddings-v2-base-code" - def test_init_interactive_installer_allows_selection(self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + def test_init_interactive_installer_applies_user_selections(self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): # Provide stable detection so defaults are deterministic monkeypatch.setattr( "semantic_code_intelligence.cli.commands.init_cmd._get_available_memory_bytes", From 0a63f8d33e78594f55242492e26035a462c97f12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:07:15 +0000 Subject: [PATCH 04/15] chore: clean up interactive init Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/95572185-7ffc-40bd-9def-cb7aa8ab330a Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/init_cmd.py | 4 +--- semantic_code_intelligence/tests/test_cli.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index 99ff68c..cf81b77 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -131,7 +131,6 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool # Check if already initialized config_dir = AppConfig.config_dir(root) - config_path = AppConfig.config_path(root) if config_dir.exists() and not interactive: print_info(f"Project already initialized at {root}") print_info(f"Config directory: {config_dir}") @@ -294,8 +293,7 @@ def _run_interactive_installer( ) chosen_profile = resolve_profile(chosen_profile_key) if chosen_profile is None: - # Should not happen due to click.Choice guard, but keep a safe fallback. - chosen_profile = default_profile + raise ValueError(f"Unknown profile key: {chosen_profile_key}") profile_changed = False if config.embedding.model_name != chosen_profile.model_name: diff --git a/semantic_code_intelligence/tests/test_cli.py b/semantic_code_intelligence/tests/test_cli.py index 7043fc3..f67d60c 100644 --- a/semantic_code_intelligence/tests/test_cli.py +++ b/semantic_code_intelligence/tests/test_cli.py @@ -110,7 +110,7 @@ def test_init_saves_recommended_batch_size(self, runner: CliRunner, tmp_path: Pa # Profile for ~3GB RAM should be precise according to registry thresholds assert config["embedding"]["model_name"] == "jinaai/jina-embeddings-v2-base-code" - def test_init_interactive_installer_applies_user_selections(self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + def test_init_interactive_applies_selections(self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): # Provide stable detection so defaults are deterministic monkeypatch.setattr( "semantic_code_intelligence.cli.commands.init_cmd._get_available_memory_bytes", From 8c0fe9cd00153f0f6f31208dcb5c3acff1c654a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:07:54 +0000 Subject: [PATCH 05/15] chore: tighten interactive input limits Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/95572185-7ffc-40bd-9def-cb7aa8ab330a Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/init_cmd.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index cf81b77..5cfd581 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -293,7 +293,8 @@ def _run_interactive_installer( ) chosen_profile = resolve_profile(chosen_profile_key) if chosen_profile is None: - raise ValueError(f"Unknown profile key: {chosen_profile_key}") + # Defensive guard; click.Choice restricts input to known keys. + raise RuntimeError(f"Unexpected missing profile for key: {chosen_profile_key}") profile_changed = False if config.embedding.model_name != chosen_profile.model_name: @@ -312,7 +313,7 @@ def _run_interactive_installer( batch_input = click.prompt( "Embedding batch size", default=recommended_batch_size, - type=click.IntRange(1, None), + type=click.IntRange(1, 1024), show_default=True, ) batch_changed = batch_input != config.embedding.batch_size From 8bc18212eb33df06808af7f1c5da49c0e62b8f6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:08:31 +0000 Subject: [PATCH 06/15] chore: define config path early Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/95572185-7ffc-40bd-9def-cb7aa8ab330a Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/init_cmd.py | 1 + 1 file changed, 1 insertion(+) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index 5cfd581..94a0ee7 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -131,6 +131,7 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool # Check if already initialized config_dir = AppConfig.config_dir(root) + config_path: Path | None = None if config_dir.exists() and not interactive: print_info(f"Project already initialized at {root}") print_info(f"Config directory: {config_dir}") From f5ab9048fb25bacd439c30a34408f655848e1a94 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:09:05 +0000 Subject: [PATCH 07/15] chore: clarify defensive profile error Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/95572185-7ffc-40bd-9def-cb7aa8ab330a Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/init_cmd.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index 94a0ee7..c476dab 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -131,7 +131,6 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool # Check if already initialized config_dir = AppConfig.config_dir(root) - config_path: Path | None = None if config_dir.exists() and not interactive: print_info(f"Project already initialized at {root}") print_info(f"Config directory: {config_dir}") @@ -295,7 +294,11 @@ def _run_interactive_installer( chosen_profile = resolve_profile(chosen_profile_key) if chosen_profile is None: # Defensive guard; click.Choice restricts input to known keys. - raise RuntimeError(f"Unexpected missing profile for key: {chosen_profile_key}") + available = ", ".join(MODEL_PROFILES.keys()) + raise RuntimeError( + f"Failed to resolve embedding profile '{chosen_profile_key}'. " + f"Available profiles: {available}. Please report this issue." + ) profile_changed = False if config.embedding.model_name != chosen_profile.model_name: From b66a94ac866c014b63deebfb448d2c1608819592 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:10:15 +0000 Subject: [PATCH 08/15] chore: simplify interactive init flow Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/95572185-7ffc-40bd-9def-cb7aa8ab330a Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- .../cli/commands/init_cmd.py | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index c476dab..849b437 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -129,26 +129,25 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool """ root = Path(path).resolve() - # Check if already initialized config_dir = AppConfig.config_dir(root) - if config_dir.exists() and not interactive: - print_info(f"Project already initialized at {root}") - print_info(f"Config directory: {config_dir}") - # Still allow --vscode and --index on existing projects - if setup_vscode: - if _generate_vscode_mcp_config(root): - print_success("VS Code MCP config written to .vscode/settings.json") - else: - print_info("VS Code MCP config already exists") - if auto_index: - _run_index(root) - return - try: if config_dir.exists(): + if not interactive: + print_info(f"Project already initialized at {root}") + print_info(f"Config directory: {config_dir}") + # Still allow --vscode and --index on existing projects + if setup_vscode: + if _generate_vscode_mcp_config(root): + print_success("VS Code MCP config written to .vscode/settings.json") + else: + print_info("VS Code MCP config already exists") + if auto_index: + _run_index(root) + return + config = load_config(root) print_info(f"Project already initialized at {root}") - print_info(f"Launching interactive installer to update configuration.") + print_info("Launching interactive installer to update configuration.") else: config, config_path = init_project(root) print_success(f"Initialized project at {root}") @@ -292,13 +291,6 @@ def _run_interactive_installer( show_choices=False, ) chosen_profile = resolve_profile(chosen_profile_key) - if chosen_profile is None: - # Defensive guard; click.Choice restricts input to known keys. - available = ", ".join(MODEL_PROFILES.keys()) - raise RuntimeError( - f"Failed to resolve embedding profile '{chosen_profile_key}'. " - f"Available profiles: {available}. Please report this issue." - ) profile_changed = False if config.embedding.model_name != chosen_profile.model_name: From 1408690e95b123f30fd8ef98a778be3cee5e4b59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:20:23 +0000 Subject: [PATCH 09/15] chore: address interactive init review Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/ff2ec324-5ea7-4843-8109-01925ea66b32 Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- .../cli/commands/init_cmd.py | 13 +++++++--- semantic_code_intelligence/tests/test_cli.py | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index 849b437..a9c89ea 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -15,6 +15,7 @@ ) from semantic_code_intelligence.embeddings.model_registry import ( CLI_PROFILE_CHOICES, + CORE_PROFILES, MODEL_PROFILES, ModelProfile, recommend_profile_for_ram, @@ -145,7 +146,13 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool _run_index(root) return - config = load_config(root) + try: + config = load_config(root) + except (json.JSONDecodeError, ValueError) as e: + print_error("Failed to read existing .codexa/config.json. Please fix or delete it and rerun 'codexa init'.") + print_error(f"Details: {e}") + ctx.exit(1) + return print_info(f"Project already initialized at {root}") print_info("Launching interactive installer to update configuration.") else: @@ -273,7 +280,7 @@ def _run_interactive_installer( table.add_column("Model") table.add_column("Description") table.add_column("Min RAM (GB)", justify="right") - for key in ["fast", "balanced", "precise"]: + for key in CORE_PROFILES: profile = MODEL_PROFILES[key] table.add_row( profile.name, @@ -286,7 +293,7 @@ def _run_interactive_installer( chosen_profile_key = click.prompt( "Select embedding profile", - type=click.Choice(["fast", "balanced", "precise"], case_sensitive=False), + type=click.Choice(CLI_PROFILE_CHOICES, case_sensitive=False), default=default_profile.name, show_choices=False, ) diff --git a/semantic_code_intelligence/tests/test_cli.py b/semantic_code_intelligence/tests/test_cli.py index f67d60c..cfe2a08 100644 --- a/semantic_code_intelligence/tests/test_cli.py +++ b/semantic_code_intelligence/tests/test_cli.py @@ -134,6 +134,30 @@ def test_init_interactive_applies_selections(self, runner: CliRunner, tmp_path: assert config["embedding"]["model_name"] == MODEL_PROFILES["fast"].model_name assert config["embedding"]["batch_size"] == 24 + def test_init_interactive_updates_existing_project(self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + # Initial setup with defaults + monkeypatch.setattr( + "semantic_code_intelligence.cli.commands.init_cmd._get_available_memory_bytes", + lambda: 2 * BYTES_PER_GB, + ) + monkeypatch.setattr( + "semantic_code_intelligence.cli.commands.init_cmd._get_cpu_count", + lambda: 2, + ) + runner.invoke(cli, ["init", str(tmp_path)]) + + # Run interactive to change profile/batch + result = runner.invoke( + cli, + ["init", str(tmp_path), "--interactive"], + input="precise\n16\n", + ) + assert result.exit_code == 0 + + config = json.loads((tmp_path / ".codexa" / "config.json").read_text(encoding="utf-8")) + assert config["embedding"]["model_name"] == MODEL_PROFILES["precise"].model_name + assert config["embedding"]["batch_size"] == 16 + class TestIndexCommand: """Tests for the index command.""" From 21a2895af326ec28b4e57bfa3547b0490e50fecf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:33:27 +0000 Subject: [PATCH 10/15] Handle interactive init config read errors Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/6a2f37c8-afbb-4ecf-b28a-07e1da012722 Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/init_cmd.py | 2 +- semantic_code_intelligence/tests/test_cli.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index a9c89ea..286e4b9 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -148,7 +148,7 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool try: config = load_config(root) - except (json.JSONDecodeError, ValueError) as e: + except (json.JSONDecodeError, ValueError, OSError) as e: print_error("Failed to read existing .codexa/config.json. Please fix or delete it and rerun 'codexa init'.") print_error(f"Details: {e}") ctx.exit(1) diff --git a/semantic_code_intelligence/tests/test_cli.py b/semantic_code_intelligence/tests/test_cli.py index cfe2a08..2bb6f38 100644 --- a/semantic_code_intelligence/tests/test_cli.py +++ b/semantic_code_intelligence/tests/test_cli.py @@ -158,6 +158,16 @@ def test_init_interactive_updates_existing_project(self, runner: CliRunner, tmp_ assert config["embedding"]["model_name"] == MODEL_PROFILES["precise"].model_name assert config["embedding"]["batch_size"] == 16 + def test_init_interactive_invalid_config(self, runner: CliRunner, tmp_path: Path): + runner.invoke(cli, ["init", str(tmp_path)]) + config_path = tmp_path / ".codexa" / "config.json" + config_path.write_text("{ invalid json", encoding="utf-8") + + result = runner.invoke(cli, ["init", str(tmp_path), "--interactive"]) + + assert result.exit_code != 0 + assert "failed to read existing .codexa/config.json" in result.output.lower() + class TestIndexCommand: """Tests for the index command.""" From 2594d0547ccc5a22784d3462e6ad064c3848c37a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:33:58 +0000 Subject: [PATCH 11/15] Add CLI coverage for interactive init config errors Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/6a2f37c8-afbb-4ecf-b28a-07e1da012722 Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/tests/test_cli.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/semantic_code_intelligence/tests/test_cli.py b/semantic_code_intelligence/tests/test_cli.py index 2bb6f38..85ef978 100644 --- a/semantic_code_intelligence/tests/test_cli.py +++ b/semantic_code_intelligence/tests/test_cli.py @@ -168,6 +168,19 @@ def test_init_interactive_invalid_config(self, runner: CliRunner, tmp_path: Path assert result.exit_code != 0 assert "failed to read existing .codexa/config.json" in result.output.lower() + def test_init_interactive_config_permission_error(self, runner: CliRunner, tmp_path: Path): + runner.invoke(cli, ["init", str(tmp_path)]) + config_path = tmp_path / ".codexa" / "config.json" + config_path.chmod(0o000) + + try: + result = runner.invoke(cli, ["init", str(tmp_path), "--interactive"]) + finally: + config_path.chmod(0o644) + + assert result.exit_code != 0 + assert "failed to read existing .codexa/config.json" in result.output.lower() + class TestIndexCommand: """Tests for the index command.""" From 6fbca28a5e39c1bfc5828229fb19cd04e58011bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:34:32 +0000 Subject: [PATCH 12/15] Stabilize interactive init permission error test Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/6a2f37c8-afbb-4ecf-b28a-07e1da012722 Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/tests/test_cli.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/semantic_code_intelligence/tests/test_cli.py b/semantic_code_intelligence/tests/test_cli.py index 85ef978..cc9eda4 100644 --- a/semantic_code_intelligence/tests/test_cli.py +++ b/semantic_code_intelligence/tests/test_cli.py @@ -168,15 +168,18 @@ def test_init_interactive_invalid_config(self, runner: CliRunner, tmp_path: Path assert result.exit_code != 0 assert "failed to read existing .codexa/config.json" in result.output.lower() - def test_init_interactive_config_permission_error(self, runner: CliRunner, tmp_path: Path): + def test_init_interactive_config_permission_error(self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): runner.invoke(cli, ["init", str(tmp_path)]) - config_path = tmp_path / ".codexa" / "config.json" - config_path.chmod(0o000) - try: - result = runner.invoke(cli, ["init", str(tmp_path), "--interactive"]) - finally: - config_path.chmod(0o644) + def raise_permission_error(*args, **kwargs): + raise OSError(errno.EACCES, "permission denied") + + monkeypatch.setattr( + "semantic_code_intelligence.cli.commands.init_cmd.load_config", + raise_permission_error, + ) + + result = runner.invoke(cli, ["init", str(tmp_path), "--interactive"]) assert result.exit_code != 0 assert "failed to read existing .codexa/config.json" in result.output.lower() From b73031083fae9a6fdee14dd78075fa30ae7282ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:55:52 +0000 Subject: [PATCH 13/15] Add guard for unresolved profile choice Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/ff7e804e-c142-4efa-848c-0c92f5534ccb Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/init_cmd.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index 286e4b9..64fa104 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -298,6 +298,11 @@ def _run_interactive_installer( show_choices=False, ) chosen_profile = resolve_profile(chosen_profile_key) + if chosen_profile is None: + raise click.ClickException( + f"Selected profile '{chosen_profile_key}' could not be resolved. " + f"Choose one of: {', '.join(CLI_PROFILE_CHOICES)}." + ) profile_changed = False if config.embedding.model_name != chosen_profile.model_name: From 1903c11c7906db0ca44588f186f6a019b220ee21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:56:25 +0000 Subject: [PATCH 14/15] Clarify unresolved profile error message Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/ff7e804e-c142-4efa-848c-0c92f5534ccb Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/init_cmd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index 64fa104..858a18e 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -300,8 +300,8 @@ def _run_interactive_installer( chosen_profile = resolve_profile(chosen_profile_key) if chosen_profile is None: raise click.ClickException( - f"Selected profile '{chosen_profile_key}' could not be resolved. " - f"Choose one of: {', '.join(CLI_PROFILE_CHOICES)}." + f"Profile '{chosen_profile_key}' could not be resolved. " + f"Valid profiles are: {', '.join(CLI_PROFILE_CHOICES)}." ) profile_changed = False From 5cca5d859e7051f1657d2945585b613e1dd7130a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:56:59 +0000 Subject: [PATCH 15/15] Clarify valid profile list in error Agent-Logs-Url: https://github.com/M9nx/CodexA/sessions/ff7e804e-c142-4efa-848c-0c92f5534ccb Co-authored-by: M9nx <90654832+M9nx@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/init_cmd.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/semantic_code_intelligence/cli/commands/init_cmd.py b/semantic_code_intelligence/cli/commands/init_cmd.py index 858a18e..60e9a31 100644 --- a/semantic_code_intelligence/cli/commands/init_cmd.py +++ b/semantic_code_intelligence/cli/commands/init_cmd.py @@ -18,6 +18,7 @@ CORE_PROFILES, MODEL_PROFILES, ModelProfile, + PROFILE_ALIASES, recommend_profile_for_ram, resolve_profile, ) @@ -299,9 +300,10 @@ def _run_interactive_installer( ) chosen_profile = resolve_profile(chosen_profile_key) if chosen_profile is None: + valid_profiles = sorted(set(MODEL_PROFILES.keys()) | set(PROFILE_ALIASES.keys())) raise click.ClickException( f"Profile '{chosen_profile_key}' could not be resolved. " - f"Valid profiles are: {', '.join(CLI_PROFILE_CHOICES)}." + f"Valid profiles are: {', '.join(valid_profiles)}." ) profile_changed = False