|
1 | 1 | import json |
| 2 | +import shutil |
2 | 3 | from pathlib import Path |
3 | 4 |
|
4 | 5 | from bcbench.agent.shared.altool_paths import ( |
|
10 | 11 | from bcbench.dataset import BaseDatasetEntry |
11 | 12 | from bcbench.exceptions import AgentError |
12 | 13 | from bcbench.logger import get_logger |
13 | | -from bcbench.types import EvaluationCategory |
| 14 | +from bcbench.types import AgentType, EvaluationCategory |
14 | 15 |
|
15 | 16 | logger = get_logger(__name__) |
16 | 17 |
|
17 | | -_AL_LSP_RELATIVE_PATH = Path(".github") / "lsp.json" |
| 18 | +# Per-task plugin folder location. Both Copilot CLI and Claude Code accept |
| 19 | +# `--plugin-dir <path>` for ad-hoc plugin loading and both look for the |
| 20 | +# manifest under `.claude-plugin/plugin.json`, so a single neutral path works |
| 21 | +# for either agent. Lives under `.bcbench/` so it's visibly BC-Bench-owned |
| 22 | +# and won't collide with either agent's auto-discovery paths. |
| 23 | +_AL_LSP_PLUGIN_RELATIVE_PATH = Path(".bcbench") / "al-lsp-plugin" |
18 | 24 |
|
19 | 25 |
|
20 | 26 | def _resolve_symbol_paths(entry: BaseDatasetEntry, category: EvaluationCategory, container_name: str) -> tuple[list[str], list[str]]: |
@@ -48,48 +54,68 @@ def _build_lsp_args(project_paths: list[str], package_cache_paths: list[str], as |
48 | 54 | return args |
49 | 55 |
|
50 | 56 |
|
51 | | -def build_lsp_config(entry: BaseDatasetEntry, category: EvaluationCategory, repo_path: Path, al_lsp: bool, container_name: str = "") -> bool: |
52 | | - """Write Copilot's project-level LSP config to <repo_path>/.github/lsp.json. |
| 57 | +def _lsp_config_for(agent_type: AgentType, args: list[str]) -> dict: |
| 58 | + """Build the agent-specific `.lsp.json` content. |
53 | 59 |
|
54 | | - When ``al_lsp=False``, removes any stale config left over from a previous run and returns False. |
55 | | - When True, writes the `lspServers.altool` entry pointing at `altool launchlspserver` and returns True. |
| 60 | + Both agents launch the same `al launchlspserver` process — only the surrounding |
| 61 | + LSP-routing schema differs: |
| 62 | +
|
| 63 | + - Copilot CLI expects `{ "lspServers": { name: { ..., "fileExtensions": {".ext": "lang"} } } }` |
| 64 | + - Claude Code expects `{ name: { ..., "extensionToLanguage": {".ext": "lang"} } }` (no wrapper, different extension key) |
| 65 | +
|
| 66 | + `command: "al"` is unqualified by design: Copilot CLI silently rejects absolute paths in LSP |
| 67 | + `command` ("Server <name> is configured but not available"), so the published `altool` wrapper |
| 68 | + (`al`) must resolve via PATH on both sides. |
| 69 | + """ |
| 70 | + server = {"command": "al", "args": args} |
| 71 | + match agent_type: |
| 72 | + case AgentType.COPILOT: |
| 73 | + return {"lspServers": {"altool": {**server, "fileExtensions": {".al": "al"}}}} |
| 74 | + case AgentType.CLAUDE: |
| 75 | + return {"altool": {**server, "extensionToLanguage": {".al": "al"}}} |
| 76 | + |
| 77 | + |
| 78 | +def build_al_lsp_plugin(entry: BaseDatasetEntry, category: EvaluationCategory, repo_path: Path, agent_type: AgentType, al_lsp: bool, container_name: str = "") -> Path | None: |
| 79 | + """Build a per-task plugin folder containing the AL LSP server, return its path or None. |
| 80 | +
|
| 81 | + Both Copilot CLI and Claude Code load this via ``--plugin-dir <path>`` for a single session |
| 82 | + — no marketplace registration, no global state, no cross-run plugin leakage. The plugin |
| 83 | + folder layout is identical between agents; only the LSP-routing schema in ``.lsp.json`` |
| 84 | + differs (see :func:`_lsp_config_for`). |
| 85 | +
|
| 86 | + Layout written under ``<repo>/.bcbench/al-lsp-plugin/``:: |
| 87 | +
|
| 88 | + .claude-plugin/plugin.json — minimal manifest (only ``name`` is required; |
| 89 | + both CLIs check this path) |
| 90 | + .lsp.json — LSP server config in the agent's schema |
| 91 | +
|
| 92 | + Returns the plugin folder path (to be passed as ``--plugin-dir``), or None when disabled. |
56 | 93 | """ |
57 | | - lsp_config_path = repo_path / _AL_LSP_RELATIVE_PATH |
| 94 | + plugin_dir = repo_path / _AL_LSP_PLUGIN_RELATIVE_PATH |
58 | 95 |
|
59 | 96 | if not al_lsp: |
60 | | - if lsp_config_path.is_file(): |
61 | | - lsp_config_path.unlink() |
62 | | - logger.info(f"Removed stale LSP config: {lsp_config_path}") |
63 | | - return False |
| 97 | + if plugin_dir.exists(): |
| 98 | + shutil.rmtree(plugin_dir) |
| 99 | + logger.info(f"Removed stale AL LSP plugin: {plugin_dir}") |
| 100 | + return None |
64 | 101 |
|
65 | 102 | project_paths = [str(repo_path / p) for p in entry.project_paths] |
66 | 103 | set_runtime_version(project_paths) |
67 | | - |
68 | 104 | package_cache_paths, assembly_probing_paths = _resolve_symbol_paths(entry, category, container_name) |
| 105 | + args = _build_lsp_args(project_paths, package_cache_paths, assembly_probing_paths) |
69 | 106 |
|
70 | | - args = _build_lsp_args( |
71 | | - project_paths=project_paths, |
72 | | - package_cache_paths=package_cache_paths, |
73 | | - assembly_probing_paths=assembly_probing_paths, |
74 | | - ) |
75 | | - |
76 | | - # Copilot CLI resolves `command` via PATH (absolute paths are silently rejected with |
77 | | - # "Server <name> is configured but not available"). `al` is the published altool |
78 | | - # wrapper installed via the .NET tool — it must be on PATH. |
79 | | - lsp_config = { |
80 | | - "lspServers": { |
81 | | - "altool": { |
82 | | - "command": "al", |
83 | | - "args": args, |
84 | | - "fileExtensions": {".al": "al"}, |
85 | | - } |
86 | | - } |
| 107 | + plugin_manifest = { |
| 108 | + "name": "al-lsp", |
| 109 | + "version": "1.0.0", |
| 110 | + "description": "AL Language Server for Business Central agentic development", |
87 | 111 | } |
| 112 | + lsp_config = _lsp_config_for(agent_type, args) |
88 | 113 |
|
89 | | - lsp_config_path.parent.mkdir(parents=True, exist_ok=True) |
90 | | - lsp_config_path.write_text(json.dumps(lsp_config, indent=2), encoding="utf-8") |
| 114 | + (plugin_dir / ".claude-plugin").mkdir(parents=True, exist_ok=True) |
| 115 | + (plugin_dir / ".claude-plugin" / "plugin.json").write_text(json.dumps(plugin_manifest, indent=2), encoding="utf-8") |
| 116 | + (plugin_dir / ".lsp.json").write_text(json.dumps(lsp_config, indent=2), encoding="utf-8") |
91 | 117 |
|
92 | | - logger.info(f"Wrote AL LSP config: {lsp_config_path}") |
| 118 | + logger.info(f"Wrote AL LSP plugin for {agent_type.value}: {plugin_dir}") |
93 | 119 | logger.debug(f"LSP configuration: {json.dumps(lsp_config, indent=2)}") |
94 | 120 |
|
95 | | - return True |
| 121 | + return plugin_dir |
0 commit comments