Skip to content

Commit 1e70a3e

Browse files
authored
Merge pull request #349 from editor-code-assistant/plugins
Support for plugins
2 parents f19dea3 + fc4edb1 commit 1e70a3e

15 files changed

Lines changed: 986 additions & 12 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ ECA Agent Guide (AGENTS.md)
3030
- Use java class typing to avoid GraalVM reflection issues
3131
- Avoid adding too many comments, only add essential or when you think is really important to mention something.
3232
- ECA's protocol specification of client <-> server lives in docs/protocol.md
33+
- If changing ECA config structure, remember to update its docs/config.json
3334
- 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.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

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

711
- Fix MCP server initialization crash (`String cannot be cast to IPersistentCollection`) when OAuth metadata endpoint returns a non-JSON or error response.

docs/config.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,57 @@
281281
}
282282
}
283283
},
284+
"plugins": {
285+
"type": "object",
286+
"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.",
287+
"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.",
288+
"examples": [
289+
{
290+
"my-org": {
291+
"source": "https://github.com/org/ai-plugins.git"
292+
},
293+
"install": ["plugin-a", "plugin-b"]
294+
}
295+
],
296+
"properties": {
297+
"install": {
298+
"type": "array",
299+
"description": "List of plugin names to install from registered sources.",
300+
"markdownDescription": "List of plugin names to install from registered sources.",
301+
"items": {
302+
"type": "string"
303+
}
304+
}
305+
},
306+
"additionalProperties": {
307+
"oneOf": [
308+
{
309+
"type": "object",
310+
"description": "A named plugin source.",
311+
"properties": {
312+
"source": {
313+
"type": "string",
314+
"description": "Git URL or local path to a plugin repository containing .eca-plugin/marketplace.json.",
315+
"markdownDescription": "Git URL or local path to a plugin repository containing `.eca-plugin/marketplace.json`.",
316+
"examples": [
317+
"https://github.com/org/ai-plugins.git",
318+
"git@github.com:org/ai-plugins.git",
319+
"/home/user/local-plugins"
320+
]
321+
}
322+
},
323+
"required": ["source"],
324+
"additionalProperties": false
325+
},
326+
{
327+
"type": "array",
328+
"items": {
329+
"type": "string"
330+
}
331+
}
332+
]
333+
}
334+
},
284335
"network": {
285336
"type": "object",
286337
"description": "Network configuration for custom CA certificates and mTLS client certificates. Values support dynamic string interpolation (e.g. '${env:SSL_CERT_FILE}').",

docs/config/plugins.md

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
---
2+
description: "Configure ECA plugins: load external skills, agents, commands, rules, hooks, MCP servers and config overrides from git repos or local paths."
3+
---
4+
5+
# Plugins / Marketplace
6+
7+
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.
8+
9+
## How it works
10+
11+
```mermaid
12+
flowchart TD
13+
A[ECA has a 'plugins' in config.json] --> B{Git URL or local path?}
14+
B -->|git| C[Clone to ~/.eca/cache/plugins]
15+
B -->|local| D[Use directory directly]
16+
C --> E[Read .eca-plugin/marketplace.json]
17+
D --> E
18+
E --> F[Read plugins from 'install']
19+
F --> G[Merge into final ECA config]
20+
```
21+
22+
1. You register one or more **sources** (git URL or local path) and list plugin names in **`install`**.
23+
2. ECA resolves each source — cloning git repos to a local cache or using the local path directly.
24+
3. Each source provides a **marketplace** (`.eca-plugin/marketplace.json`) listing its available plugins.
25+
4. ECA matches `install` names against the marketplace, then **discovers components** from each matched plugin directory.
26+
5. All components are **merged** into the config waterfall — user config always takes precedence on conflicts.
27+
28+
## Commands
29+
30+
### `/plugins`
31+
32+
Lists all available plugins from your configured marketplaces. Plugins that are already installed are marked with ✅.
33+
34+
```
35+
/plugins
36+
```
37+
38+
### `/plugin-install`
39+
40+
Installs a plugin by adding it to the `install` list in your global config.
41+
42+
```
43+
/plugin-install <plugin-name>
44+
/plugin-install <plugin-name@marketplace>
45+
```
46+
47+
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.
48+
49+
## Pointing to a plugin source / marketplace
50+
51+
Add a `plugins` key to your config with one or more named sources and an `install` array:
52+
53+
=== "Git source"
54+
55+
```javascript title="~/.config/eca/config.json"
56+
{
57+
"plugins": {
58+
"my-org": {
59+
"source": "https://github.com/my-org/eca-plugins.git"
60+
},
61+
"install": ["code-review", "security-scanner"]
62+
}
63+
}
64+
```
65+
66+
=== "Local path (for development)"
67+
68+
```javascript title=".eca/config.json"
69+
{
70+
"plugins": {
71+
"local-dev": {
72+
"source": "/home/user/my-eca-plugins"
73+
},
74+
"install": ["my-plugin"]
75+
}
76+
}
77+
```
78+
79+
=== "Multiple sources"
80+
81+
ECA searches all registered sources when resolving `install` entries:
82+
83+
```javascript title="~/.config/eca/config.json"
84+
{
85+
"plugins": {
86+
"company": {
87+
"source": "https://github.com/company/eca-plugins.git"
88+
},
89+
"community": {
90+
"source": "https://github.com/community/shared-plugins.git"
91+
},
92+
"install": ["company-standards", "linter-setup", "shared-skills"]
93+
}
94+
}
95+
```
96+
97+
## Creating a plugin source (Plugins marketplace)
98+
99+
A plugin source is a directory (typically a git repo) with a `.eca-plugin/marketplace.json` file that lists available plugins.
100+
101+
### Marketplace file
102+
103+
```json title=".eca-plugin/marketplace.json"
104+
{
105+
"plugins": [
106+
{
107+
"name": "code-review",
108+
"description": "Agents and skills for thorough code review",
109+
"source": "plugins/code-review"
110+
},
111+
{
112+
"name": "security-scanner",
113+
"description": "Security-focused rules and hooks",
114+
"source": "plugins/security-scanner"
115+
}
116+
]
117+
}
118+
```
119+
120+
Each plugin entry has:
121+
122+
| Field | Description |
123+
|-------|-------------|
124+
| `name` | Unique plugin name (used in `install`) |
125+
| `description` | Human-readable description |
126+
| `source` | Relative path from the repo root to the plugin directory |
127+
128+
### Plugin directory structure
129+
130+
Each plugin directory can contain any combination of:
131+
132+
```
133+
plugins/code-review/
134+
├── skills/
135+
│ └── review-checklist/
136+
│ └── SKILL.md
137+
├── agents/
138+
│ └── reviewer.md
139+
├── commands/
140+
│ └── review.md
141+
├── rules/
142+
│ └── code-standards.md
143+
├── hooks/
144+
│ └── hooks.json
145+
├── .mcp.json
146+
└── eca.json
147+
```
148+
149+
| Path | What it provides | Details |
150+
|------|-----------------|---------|
151+
| `skills/` | Skill definitions | Each subfolder follows the [agentskills.io](https://agentskills.io/) spec with a `SKILL.md` |
152+
| `agents/*.md` | Agent definitions | Markdown files with YAML frontmatter, same format as local agents |
153+
| `commands/*.md` | Custom commands | Markdown command files, same format as local commands |
154+
| `rules/**` | Rule files | Any files under `rules/` are loaded as rules |
155+
| `hooks/hooks.json` | Hooks | [ECA hook format](hooks.md) |
156+
| `.mcp.json` | MCP server definitions | Standard `{"mcpServers": {...}}` format |
157+
| `eca.json` | Config overrides | Arbitrary ECA config keys deep-merged into config |
158+
159+
All paths are optional — include only what your plugin needs.
160+
161+
=== "Skill-only plugin"
162+
163+
```
164+
plugins/gif-maker/
165+
└── skills/
166+
└── gif-generator/
167+
├── SKILL.md
168+
└── scripts/
169+
└── generate.py
170+
```
171+
172+
=== "Hooks + MCP plugin"
173+
174+
```
175+
plugins/security-scanner/
176+
├── hooks/
177+
│ └── hooks.json
178+
└── .mcp.json
179+
```
180+
181+
=== "Config overrides only"
182+
183+
```
184+
plugins/team-defaults/
185+
└── eca.json
186+
```
187+
188+
=== "Full plugin"
189+
190+
```
191+
plugins/company-standards/
192+
├── skills/
193+
│ └── internal-api/
194+
│ └── SKILL.md
195+
├── agents/
196+
│ └── reviewer.md
197+
├── commands/
198+
│ └── deploy.md
199+
├── rules/
200+
│ └── coding-standards.md
201+
├── hooks/
202+
│ └── hooks.json
203+
├── .mcp.json
204+
└── eca.json
205+
```

integration-test/integration/chat/commands_test.clj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
{:name "doctor" :arguments []}
3333
{:name "repo-map-show" :arguments []}
3434
{:name "prompt-show" :arguments [{:name "optional-prompt"}]}
35-
{:name "subagents" :arguments []}]}
35+
{:name "subagents" :arguments []}
36+
{:name "plugins" :arguments []}
37+
{:name "plugin-install" :arguments [{:name "plugin"}]}]}
3638
resp))))
3739

3840
(testing "We query specific commands"
@@ -45,7 +47,8 @@
4547
{:name "costs" :arguments []}
4648
{:name "compact" :arguments [{:name "additional-input"}]}
4749
{:name "config" :arguments []}
48-
{:name "subagents" :arguments []}]}
50+
{:name "subagents" :arguments []}
51+
{:name "plugins" :arguments []}]}
4952
resp))))
5053

5154
(testing "We send a built-in command"

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ nav:
2424
- Completion: config/completion.md
2525
- Rewrite: config/rewrite.md
2626
- Context Management: config/context-management.md
27+
- Plugins / Marketplace: config/plugins.md
2728
- Metrics: config/metrics.md
2829
- Network / Enterprise: config/network.md
2930
- User Examples: config/examples.md

src/eca/cache.clj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,18 @@
4141

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

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

51+
(defn plugins-dir
52+
"Returns the base directory for caching cloned plugin sources."
53+
^java.io.File []
54+
(io/file (global-dir) plugins-dir-name))
55+
5056
(defn save-tool-call-output!
5157
"Saves the full tool call output text to a cache file.
5258
Returns the absolute path of the saved file as a string."

0 commit comments

Comments
 (0)