Skip to content

Commit 7bc5571

Browse files
committed
Add /plugin-install command
1 parent 96fecc7 commit 7bc5571

3 files changed

Lines changed: 68 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- Fix MCP server initialization crash (`String cannot be cast to IPersistentCollection`) when OAuth metadata endpoint returns a non-JSON or error response.
66
- Add `plugins` config for loading external configuration from git repos or local paths. #349
77
- Plugins can provide skills, MCP servers, agents, commands, hooks, rules, and arbitrary config overrides.
8-
- Add commands `/plugins-list` and `/plugins-add`.
8+
- Add commands `/plugins-list` and `/plugin-install`.
99

1010
## 0.110.3
1111

src/eca/features/commands.clj

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,11 @@
140140
{:name "plugins-list"
141141
:type :native
142142
:description "List available plugins from configured marketplaces."
143-
:arguments []}]
143+
:arguments []}
144+
{:name "plugin-install"
145+
:type :native
146+
:description "Install a plugin (e.g. /plugin-install my-plugin or /plugin-install my-plugin@marketplace)"
147+
:arguments [{:name "plugin" :description "Plugin name or plugin@marketplace"}]}]
144148
custom-cmds (map (fn [custom]
145149
{:name (:name custom)
146150
:type :custom-prompt
@@ -434,6 +438,13 @@
434438
"No plugin marketplaces configured. Add plugin sources to your config under the `plugins` key.")]
435439
{:type :chat-messages
436440
:chats {chat-id {:messages [{:role "system" :content [{:type :text :text msg}]}]}}})
441+
"plugin-install" (let [plugin-input (first args)
442+
result (if (string/blank? plugin-input)
443+
{:status :error
444+
:message "Usage: `/plugin-install <plugin-name>` or `/plugin-install <plugin-name@marketplace>`"}
445+
(f.plugins/install-plugin! (:plugins config) plugin-input))]
446+
{:type :chat-messages
447+
:chats {chat-id {:messages [{:role "system" :content [{:type :text :text (:message result)}]}]}}})
437448

438449
;; else check if a custom command or skill
439450
(if-let [custom-command-prompt (get-custom-command command args custom-cmds)]

src/eca/features/plugins.clj

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
[clojure.java.io :as io]
1313
[clojure.string :as string]
1414
[eca.cache :as cache]
15+
[eca.config :as config]
1516
[eca.features.agents :as agents]
1617
[eca.logger :as logger]
1718
[eca.shared :as shared]))
@@ -340,3 +341,57 @@
340341
:source-name source-name
341342
:source-url source-url
342343
:installed? (contains? installed-set (:name plugin))})))))
344+
345+
(defn ^:private parse-plugin-arg
346+
"Parses a plugin install argument. Supports 'plugin-name' or 'plugin-name@marketplace'.
347+
Returns {:plugin-name ... :marketplace ...} where :marketplace may be nil."
348+
[^String arg]
349+
(let [parts (string/split arg #"@" 2)]
350+
{:plugin-name (first parts)
351+
:marketplace (when (= 2 (count parts)) (second parts))}))
352+
353+
(defn ^:private find-plugin-in-marketplaces
354+
"Finds a plugin by name across all resolved marketplaces, optionally filtered by source name.
355+
Returns {:name :source-name :source-url} or nil."
356+
[plugins-config plugin-name marketplace-filter]
357+
(let [sources (parse-sources plugins-config)]
358+
(first
359+
(for [[source-name source-url] sources
360+
:when (or (nil? marketplace-filter) (= marketplace-filter source-name))
361+
:let [source-dir (resolve-source! source-url)]
362+
:when source-dir
363+
:let [marketplace (read-marketplace source-dir)]
364+
:when marketplace
365+
:let [entry (find-plugin-entry plugin-name marketplace)]
366+
:when entry]
367+
{:name plugin-name
368+
:source-name source-name
369+
:source-url source-url}))))
370+
371+
(defn install-plugin!
372+
"Installs a plugin by adding it to the global config install list.
373+
`input` is either 'plugin-name' or 'plugin-name@marketplace'.
374+
Returns {:status :ok/:error, :message ...}."
375+
[plugins-config ^String input]
376+
(let [{:keys [plugin-name marketplace]} (parse-plugin-arg input)
377+
sources (parse-sources plugins-config)
378+
current-install (set (get plugins-config :install []))]
379+
(cond
380+
(empty? sources)
381+
{:status :error
382+
:message "No plugin marketplaces configured. Add plugin sources to your config under the `plugins` key."}
383+
384+
(contains? current-install plugin-name)
385+
{:status :error
386+
:message (str "Plugin `" plugin-name "` is already installed.")}
387+
388+
:else
389+
(if-let [found (find-plugin-in-marketplaces plugins-config plugin-name marketplace)]
390+
(let [new-install (vec (sort (conj current-install plugin-name)))]
391+
(config/update-global-config! {:plugins {:install new-install}})
392+
{:status :ok
393+
:message (str "Plugin `" plugin-name "` installed from **" (:source-name found) "**. Restart ECA to activate it.")})
394+
{:status :error
395+
:message (if marketplace
396+
(str "Plugin `" plugin-name "` not found in marketplace `" marketplace "`.")
397+
(str "Plugin `" plugin-name "` not found in any configured marketplace."))}))))

0 commit comments

Comments
 (0)