Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ ECA Agent Guide (AGENTS.md)
- Use java class typing to avoid GraalVM reflection issues
- Avoid adding too many comments, only add essential or when you think is really important to mention something.
- ECA's protocol specification of client <-> server lives in docs/protocol.md
- If changing ECA config structure, remember to update its docs/config.json
- When adding support to a new feature or fixing a existing github issue, add a entry to Unreleased in CHANGELOG.md if not already there, be concise like the rest.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- Add `plugins` config for loading external configuration from git repos or local paths. #349
- Plugins can provide skills, MCP servers, agents, commands, hooks, rules, and arbitrary config overrides.
- Add commands `/plugins` and `/plugin-install`.

## 0.111.0

- Fix MCP server initialization crash (`String cannot be cast to IPersistentCollection`) when OAuth metadata endpoint returns a non-JSON or error response.
Expand Down
51 changes: 51 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,57 @@
}
}
},
"plugins": {
"type": "object",
"description": "Plugin system for loading external configuration from git repos or local paths. Each key (except 'install') is a named plugin source with a 'source' URL or path. 'install' lists plugin names to install from any registered source.",
"markdownDescription": "Plugin system for loading external configuration from git repos or local paths. Each key (except `install`) is a named plugin source with a `source` URL or path. `install` lists plugin names to install from any registered source.",
"examples": [
{
"my-org": {
"source": "https://github.com/org/ai-plugins.git"
},
"install": ["plugin-a", "plugin-b"]
}
],
"properties": {
"install": {
"type": "array",
"description": "List of plugin names to install from registered sources.",
"markdownDescription": "List of plugin names to install from registered sources.",
"items": {
"type": "string"
}
}
},
"additionalProperties": {
"oneOf": [
{
"type": "object",
"description": "A named plugin source.",
"properties": {
"source": {
"type": "string",
"description": "Git URL or local path to a plugin repository containing .eca-plugin/marketplace.json.",
"markdownDescription": "Git URL or local path to a plugin repository containing `.eca-plugin/marketplace.json`.",
"examples": [
"https://github.com/org/ai-plugins.git",
"git@github.com:org/ai-plugins.git",
"/home/user/local-plugins"
]
}
},
"required": ["source"],
"additionalProperties": false
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"network": {
"type": "object",
"description": "Network configuration for custom CA certificates and mTLS client certificates. Values support dynamic string interpolation (e.g. '${env:SSL_CERT_FILE}').",
Expand Down
205 changes: 205 additions & 0 deletions docs/config/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
description: "Configure ECA plugins: load external skills, agents, commands, rules, hooks, MCP servers and config overrides from git repos or local paths."
---

# Plugins / Marketplace

Plugins let you share and reuse ECA configuration across projects and teams. A plugin source is a git repository or local directory containing a **marketplace** of plugins, each providing any combination of skills, agents, commands, rules, hooks, MCP servers, and config overrides.

## How it works

```mermaid
flowchart TD
A[ECA has a 'plugins' in config.json] --> B{Git URL or local path?}
B -->|git| C[Clone to ~/.eca/cache/plugins]
B -->|local| D[Use directory directly]
C --> E[Read .eca-plugin/marketplace.json]
D --> E
E --> F[Read plugins from 'install']
F --> G[Merge into final ECA config]
```

1. You register one or more **sources** (git URL or local path) and list plugin names in **`install`**.
2. ECA resolves each source — cloning git repos to a local cache or using the local path directly.
3. Each source provides a **marketplace** (`.eca-plugin/marketplace.json`) listing its available plugins.
4. ECA matches `install` names against the marketplace, then **discovers components** from each matched plugin directory.
5. All components are **merged** into the config waterfall — user config always takes precedence on conflicts.

## Commands

### `/plugins`

Lists all available plugins from your configured marketplaces. Plugins that are already installed are marked with ✅.

```
/plugins
```

### `/plugin-install`

Installs a plugin by adding it to the `install` list in your global config.

```
/plugin-install <plugin-name>
/plugin-install <plugin-name@marketplace>
```

Use `<plugin-name@marketplace>` to disambiguate when multiple sources provide a plugin with the same name. After installing, restart ECA for the plugin to take effect.

## Pointing to a plugin source / marketplace

Add a `plugins` key to your config with one or more named sources and an `install` array:

=== "Git source"

```javascript title="~/.config/eca/config.json"
{
"plugins": {
"my-org": {
"source": "https://github.com/my-org/eca-plugins.git"
},
"install": ["code-review", "security-scanner"]
}
}
```

=== "Local path (for development)"

```javascript title=".eca/config.json"
{
"plugins": {
"local-dev": {
"source": "/home/user/my-eca-plugins"
},
"install": ["my-plugin"]
}
}
```

=== "Multiple sources"

ECA searches all registered sources when resolving `install` entries:

```javascript title="~/.config/eca/config.json"
{
"plugins": {
"company": {
"source": "https://github.com/company/eca-plugins.git"
},
"community": {
"source": "https://github.com/community/shared-plugins.git"
},
"install": ["company-standards", "linter-setup", "shared-skills"]
}
}
```

## Creating a plugin source (Plugins marketplace)

A plugin source is a directory (typically a git repo) with a `.eca-plugin/marketplace.json` file that lists available plugins.

### Marketplace file

```json title=".eca-plugin/marketplace.json"
{
"plugins": [
{
"name": "code-review",
"description": "Agents and skills for thorough code review",
"source": "plugins/code-review"
},
{
"name": "security-scanner",
"description": "Security-focused rules and hooks",
"source": "plugins/security-scanner"
}
]
}
```

Each plugin entry has:

| Field | Description |
|-------|-------------|
| `name` | Unique plugin name (used in `install`) |
| `description` | Human-readable description |
| `source` | Relative path from the repo root to the plugin directory |

### Plugin directory structure

Each plugin directory can contain any combination of:

```
plugins/code-review/
├── skills/
│ └── review-checklist/
│ └── SKILL.md
├── agents/
│ └── reviewer.md
├── commands/
│ └── review.md
├── rules/
│ └── code-standards.md
├── hooks/
│ └── hooks.json
├── .mcp.json
└── eca.json
```

| Path | What it provides | Details |
|------|-----------------|---------|
| `skills/` | Skill definitions | Each subfolder follows the [agentskills.io](https://agentskills.io/) spec with a `SKILL.md` |
| `agents/*.md` | Agent definitions | Markdown files with YAML frontmatter, same format as local agents |
| `commands/*.md` | Custom commands | Markdown command files, same format as local commands |
| `rules/**` | Rule files | Any files under `rules/` are loaded as rules |
| `hooks/hooks.json` | Hooks | [ECA hook format](hooks.md) |
| `.mcp.json` | MCP server definitions | Standard `{"mcpServers": {...}}` format |
| `eca.json` | Config overrides | Arbitrary ECA config keys deep-merged into config |

All paths are optional — include only what your plugin needs.

=== "Skill-only plugin"

```
plugins/gif-maker/
└── skills/
└── gif-generator/
├── SKILL.md
└── scripts/
└── generate.py
```

=== "Hooks + MCP plugin"

```
plugins/security-scanner/
├── hooks/
│ └── hooks.json
└── .mcp.json
```

=== "Config overrides only"

```
plugins/team-defaults/
└── eca.json
```

=== "Full plugin"

```
plugins/company-standards/
├── skills/
│ └── internal-api/
│ └── SKILL.md
├── agents/
│ └── reviewer.md
├── commands/
│ └── deploy.md
├── rules/
│ └── coding-standards.md
├── hooks/
│ └── hooks.json
├── .mcp.json
└── eca.json
```
7 changes: 5 additions & 2 deletions integration-test/integration/chat/commands_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
{:name "doctor" :arguments []}
{:name "repo-map-show" :arguments []}
{:name "prompt-show" :arguments [{:name "optional-prompt"}]}
{:name "subagents" :arguments []}]}
{:name "subagents" :arguments []}
{:name "plugins" :arguments []}
{:name "plugin-install" :arguments [{:name "plugin"}]}]}
resp))))

(testing "We query specific commands"
Expand All @@ -45,7 +47,8 @@
{:name "costs" :arguments []}
{:name "compact" :arguments [{:name "additional-input"}]}
{:name "config" :arguments []}
{:name "subagents" :arguments []}]}
{:name "subagents" :arguments []}
{:name "plugins" :arguments []}]}
resp))))

(testing "We send a built-in command"
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ nav:
- Completion: config/completion.md
- Rewrite: config/rewrite.md
- Context Management: config/context-management.md
- Plugins / Marketplace: config/plugins.md
- Metrics: config/metrics.md
- Network / Enterprise: config/network.md
- User Examples: config/examples.md
Expand Down
6 changes: 6 additions & 0 deletions src/eca/cache.clj
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,18 @@

(def ^:private logger-tag "[CACHE]")
(def ^:private tool-call-outputs-dir-name "toolCallOutputs")
(def ^:private plugins-dir-name "plugins")

(defn tool-call-outputs-dir
"Returns the File object for the tool call outputs cache directory."
^File []
(io/file (global-dir) tool-call-outputs-dir-name))

(defn plugins-dir
"Returns the base directory for caching cloned plugin sources."
^java.io.File []
(io/file (global-dir) plugins-dir-name))

(defn save-tool-call-output!
"Saves the full tool call output text to a cache file.
Returns the absolute path of the saved file as a string."
Expand Down
Loading
Loading