Skip to content

Commit caf6a6e

Browse files
committed
Support for plugins
1 parent fcfe91d commit caf6a6e

13 files changed

Lines changed: 844 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ ECA Agent Guide (AGENTS.md)
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
3333
- 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.
34+
- If changing ECA config structure, remember to update its docs/config.json

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## Unreleased
44

55
- Fix MCP server initialization crash (`String cannot be cast to IPersistentCollection`) when OAuth metadata endpoint returns a non-JSON or error response.
6+
- Add `plugins` config for loading external configuration from git repos or local paths.
7+
- Plugins can provide skills, MCP servers, agents, commands, hooks, rules, and arbitrary config overrides.
68

79
## 0.110.3
810

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: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
## Pointing to a plugin source / marketplace
29+
30+
Add a `plugins` key to your config with one or more named sources and an `install` array:
31+
32+
=== "Git source"
33+
34+
```javascript title="~/.config/eca/config.json"
35+
{
36+
"plugins": {
37+
"my-org": {
38+
"source": "https://github.com/my-org/eca-plugins.git"
39+
},
40+
"install": ["code-review", "security-scanner"]
41+
}
42+
}
43+
```
44+
45+
=== "Local path (for development)"
46+
47+
```javascript title=".eca/config.json"
48+
{
49+
"plugins": {
50+
"local-dev": {
51+
"source": "/home/user/my-eca-plugins"
52+
},
53+
"install": ["my-plugin"]
54+
}
55+
}
56+
```
57+
58+
=== "Multiple sources"
59+
60+
ECA searches all registered sources when resolving `install` entries:
61+
62+
```javascript title="~/.config/eca/config.json"
63+
{
64+
"plugins": {
65+
"company": {
66+
"source": "https://github.com/company/eca-plugins.git"
67+
},
68+
"community": {
69+
"source": "https://github.com/community/shared-plugins.git"
70+
},
71+
"install": ["company-standards", "linter-setup", "shared-skills"]
72+
}
73+
}
74+
```
75+
76+
## Creating a plugin source (Plugins marketplace)
77+
78+
A plugin source is a directory (typically a git repo) with a `.eca-plugin/marketplace.json` file that lists available plugins.
79+
80+
### Marketplace file
81+
82+
```json title=".eca-plugin/marketplace.json"
83+
{
84+
"plugins": [
85+
{
86+
"name": "code-review",
87+
"description": "Agents and skills for thorough code review",
88+
"source": "plugins/code-review"
89+
},
90+
{
91+
"name": "security-scanner",
92+
"description": "Security-focused rules and hooks",
93+
"source": "plugins/security-scanner"
94+
}
95+
]
96+
}
97+
```
98+
99+
Each plugin entry has:
100+
101+
| Field | Description |
102+
|-------|-------------|
103+
| `name` | Unique plugin name (used in `install`) |
104+
| `description` | Human-readable description |
105+
| `source` | Relative path from the repo root to the plugin directory |
106+
107+
### Plugin directory structure
108+
109+
Each plugin directory can contain any combination of:
110+
111+
```
112+
plugins/code-review/
113+
├── skills/
114+
│ └── review-checklist/
115+
│ └── SKILL.md
116+
├── agents/
117+
│ └── reviewer.md
118+
├── commands/
119+
│ └── review.md
120+
├── rules/
121+
│ └── code-standards.md
122+
├── hooks/
123+
│ └── hooks.json
124+
├── .mcp.json
125+
└── eca.json
126+
```
127+
128+
| Path | What it provides | Details |
129+
|------|-----------------|---------|
130+
| `skills/` | Skill definitions | Each subfolder follows the [agentskills.io](https://agentskills.io/) spec with a `SKILL.md` |
131+
| `agents/*.md` | Agent definitions | Markdown files with YAML frontmatter, same format as local agents |
132+
| `commands/*.md` | Custom commands | Markdown command files, same format as local commands |
133+
| `rules/**` | Rule files | Any files under `rules/` are loaded as rules |
134+
| `hooks/hooks.json` | Hooks | [ECA hook format](hooks.md) |
135+
| `.mcp.json` | MCP server definitions | Standard `{"mcpServers": {...}}` format |
136+
| `eca.json` | Config overrides | Arbitrary ECA config keys deep-merged into config |
137+
138+
All paths are optional — include only what your plugin needs.
139+
140+
=== "Skill-only plugin"
141+
142+
```
143+
plugins/gif-maker/
144+
└── skills/
145+
└── gif-generator/
146+
├── SKILL.md
147+
└── scripts/
148+
└── generate.py
149+
```
150+
151+
=== "Hooks + MCP plugin"
152+
153+
```
154+
plugins/security-scanner/
155+
├── hooks/
156+
│ └── hooks.json
157+
└── .mcp.json
158+
```
159+
160+
=== "Config overrides only"
161+
162+
```
163+
plugins/team-defaults/
164+
└── eca.json
165+
```
166+
167+
=== "Full plugin"
168+
169+
```
170+
plugins/company-standards/
171+
├── skills/
172+
│ └── internal-api/
173+
│ └── SKILL.md
174+
├── agents/
175+
│ └── reviewer.md
176+
├── commands/
177+
│ └── deploy.md
178+
├── rules/
179+
│ └── coding-standards.md
180+
├── hooks/
181+
│ └── hooks.json
182+
├── .mcp.json
183+
└── eca.json
184+
```

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."

src/eca/config.clj

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@
192192
:completion {:model "openai/gpt-4.1"}
193193
:netrcFile nil
194194
:autoCompactPercentage 75
195+
:plugins {}
195196
:env "prod"})
196197

197198
(defn ^:private parse-dynamic-string-values
@@ -306,6 +307,8 @@
306307

307308
(def initialization-config* (atom {}))
308309

310+
(def plugin-components* (atom nil))
311+
309312
(defn ^:private deep-merge [& maps]
310313
(apply merge-with (fn [& args]
311314
(if (every? #(or (map? %) (nil? %)) args)
@@ -411,7 +414,8 @@
411414
[:behavior :ANY :toolCall :approval :ask :ANY :argsMatchers]
412415
[:behavior :ANY :toolCall :approval :deny]
413416
[:behavior :ANY :toolCall :approval :deny :ANY :argsMatchers]
414-
[:otlp]]})
417+
[:otlp]
418+
[:plugins]]})
415419

416420
(defn ^:private migrate-legacy-agent-name
417421
"Migrates legacy agent names 'agent' and 'build' to 'code'."
@@ -455,7 +459,15 @@
455459
(let [initialization-config @initialization-config*
456460
pure-config? (:pureConfig initialization-config)
457461
merge-config (fn [c1 c2]
458-
(deep-merge c1 (normalize-fields normalization-rules c2)))]
462+
(deep-merge c1 (normalize-fields normalization-rules c2)))
463+
plugin-data (when-not pure-config? @plugin-components*)
464+
plugin-config (when plugin-data
465+
(let [cfg (:config-fragment plugin-data)]
466+
;; commands/rules are vectors — separate them to avoid deep-merge replacement
467+
(dissoc cfg :commands :rules)))
468+
plugin-commands (:commands plugin-data)
469+
plugin-rules (:rules plugin-data)
470+
plugin-agents (:agents plugin-data)]
459471
(-> (as-> {} $
460472
(merge-config $ (initial-config))
461473
(merge-config $ initialization-config)
@@ -465,16 +477,22 @@
465477
(merge-config $ (when-not pure-config? custom-config))
466478
(-> $
467479
(merge-config (when-not pure-config? (config-from-global-file)))
468-
(merge-config (when-not pure-config? (config-from-local-file (:workspace-folders db)))))))
480+
(merge-config (when-not pure-config? (config-from-local-file (:workspace-folders db))))))
481+
;; Plugin config merges after all file configs (user local config wins via later merge)
482+
(merge-config $ plugin-config))
483+
;; Append plugin commands/rules (vector concat, not deep-merge replace)
484+
(cond->
485+
(seq plugin-commands) (update :commands #(vec (concat % plugin-commands)))
486+
(seq plugin-rules) (update :rules #(vec (concat % plugin-rules))))
469487
migrate-legacy-config
470488
;; Merge markdown-defined agents (lowest priority — JSON config agents win)
489+
;; Plugin agents merge at same level as markdown agents
471490
(as-> config
472491
(let [md-agent-configs (when-not pure-config?
473-
;; TODO how to avoid this dependency?
474-
(agents/all-md-agents (:workspace-folders db)))]
475-
(if (seq md-agent-configs)
492+
(agents/all-md-agents (:workspace-folders db)))]
493+
(if (or (seq md-agent-configs) (seq plugin-agents))
476494
(update config :agent (fn [existing]
477-
(merge md-agent-configs existing)))
495+
(merge md-agent-configs plugin-agents existing)))
478496
config))))))
479497

480498
(def all (memoize/ttl all* :ttl/threshold ttl-cache-config-ms))

src/eca/features/agents.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
(get tools-map "ask")
4343
(assoc-in [:approval :ask] (tools-list->approval-map (get tools-map "ask"))))))))
4444

45-
(defn ^:private agent-md-file->agent
45+
(defn agent-md-file->agent
4646
[md-file]
4747
(try
4848
(let [agent-name (string/lower-case (fs/strip-ext (fs/file-name md-file)))

0 commit comments

Comments
 (0)