diff --git a/docs/config.md b/docs/config.md index af122d04..f0359d57 100644 --- a/docs/config.md +++ b/docs/config.md @@ -908,6 +908,7 @@ from any working directory: | `agent.cache_ttl` | string | `"5m"` | Prompt-cache write TTL policy: `5m` (status quo), `1h` (always request the 1-hour TTL), or `auto` (per session at client-build time: sparse-cadence sessions — persistent crons, wakeup loops, spaced chats — get `1h`; dense sessions stay on `5m`). Per-cron-job override via `cache_ttl` in jobs.yaml. See `nerve/agent/cache_policy.py` | | `agent.cache_ttl_excluded_models` | list | `[]` | Model-name substrings that never request the 1h TTL | | `agent.agent_teams` | bool | `true` | Set `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` for the CLI subprocess, which registers the `SendMessage` tool. The Agent tool advertises `SendMessage` for resuming a sub-agent whether or not the flag is set, so with it off the model reaches for a tool that does not exist. Nerve loads no settings files (`setting_sources=[]`), so the env dict is the flag's only route in. Teammates stay opt-in per turn and cost a full context window each; the CLI cannot restore in-process teammates when a session's client is recycled (idle timeout, restart, crash retry) | +| `agent.claude_plugin_dirs` | list | `[]` | Extra local Claude Code plugin directories, passed to the CLI as `--plugin-dir` on top of plugins auto-discovered from `~/.claude` (`enabledPlugins` whose cache dir ships `.mcp.json`). Each dir must contain `.claude-plugin/plugin.json` and may declare `lspServers` (e.g. gopls feeding compiler diagnostics into edit results), `mcpServers`, commands, agents, or skills. Auto-discovery cannot forward marketplace LSP plugins (their cache dirs carry no manifest) and the CLI runs with `setting_sources=[]`, so this key is the only route in for LSP servers. Dirs without a manifest are skipped with a warning. Like every `agent.*` key, YAML edits land via the full config reload (`nerve reload`) and apply to sessions started after it — the MCP-only reload (`mcp_reload` tool, `POST /api/mcp-servers/reload`) re-reads plugin dirs from the in-memory config | | `agent.prompt_rewrite.enabled` | bool | `true` | Offer the first-prompt rewrite feature in the web UI (per-user toggle lives in the composer) | | `agent.prompt_rewrite.model` | string | `""` | Model for prompt rewriting (empty = `agent.model`, the chat model) | | `agent.prompt_rewrite.max_tokens` | int | `1024` | Max tokens for the rewritten prompt | diff --git a/nerve/agent/engine.py b/nerve/agent/engine.py index 08bbe861..d711a953 100644 --- a/nerve/agent/engine.py +++ b/nerve/agent/engine.py @@ -491,7 +491,9 @@ async def initialize(self) -> None: # Load Claude Code plugin directories for SDK plugins field from nerve.config import load_claude_code_plugins - self._claude_code_plugins = load_claude_code_plugins() + self._claude_code_plugins = load_claude_code_plugins( + extra_dirs=self.config.agent.claude_plugin_dirs, + ) # Sync MCP servers to DB for frontend visibility await self._sync_mcp_servers_to_db() @@ -542,7 +544,9 @@ async def reload_mcp_config(self) -> list: # reload sees the same config.yaml + workspace/config/settings.yaml that # startup loaded. self._mcp_servers_cache = load_mcp_servers(self.config.config_dir) - self._claude_code_plugins = load_claude_code_plugins() + self._claude_code_plugins = load_claude_code_plugins( + extra_dirs=self.config.agent.claude_plugin_dirs, + ) await self._sync_mcp_servers_to_db() logger.info( "MCP config reloaded: %d server(s), %d Claude Code plugin(s)", diff --git a/nerve/config.py b/nerve/config.py index dc192688..9460047a 100644 --- a/nerve/config.py +++ b/nerve/config.py @@ -887,6 +887,14 @@ class AgentConfig: # message teammates that no longer exist. Set False to restore the CLI # default (no SendMessage, no teams). agent_teams: bool = True + # Extra local Claude Code plugin directories, passed to the CLI as + # --plugin-dir on top of plugins auto-discovered from ~/.claude. Each + # directory must contain .claude-plugin/plugin.json and may declare + # lspServers, mcpServers, commands, agents, or skills. Auto-discovery + # only forwards plugins whose cache dir ships .mcp.json, and the CLI + # runs with setting_sources=[] so it never reads ~/.claude/settings.json + # itself — this key is the only route in for LSP-type plugins. + claude_plugin_dirs: list[str] = field(default_factory=list) prompt_rewrite: PromptRewriteConfig = field(default_factory=PromptRewriteConfig) @property @@ -925,6 +933,7 @@ def from_dict(cls, d: dict) -> AgentConfig: cli_idle_timeout_seconds=d.get("cli_idle_timeout_seconds", 900), background_agent_permissions=d.get("background_agent_permissions", True), agent_teams=d.get("agent_teams", True), + claude_plugin_dirs=_str_list(d.get("claude_plugin_dirs"), clean=True), prompt_rewrite=PromptRewriteConfig.from_dict(d.get("prompt_rewrite") or {}), ) @@ -2444,17 +2453,32 @@ def _get_enabled_claude_code_plugins( def load_claude_code_plugins( claude_dir: Path | None = None, + extra_dirs: list[str] | None = None, ) -> list[dict[str, str]]: """Return SDK-compatible plugin configs for enabled Claude Code plugins. Each entry is ``{"type": "local", "path": "