refactor: harness CLI — объект первым, действие вторым
- грамматика: harness <объект> <действие> (skills / agents / mcp / catalog / hermes / opencode) - skills list теперь показывает каталог: что доступно к установке, с отметками ✓/· - skills installed — прежний вид «что реально стоит» (включая не из каталога) - фикс: установленные скиллы не находились — Path.rglob не разворачивает симлинки, перешёл на os.walk(followlinks=True) - в листинге каталога виден sha источника и признак «⚠ сдвинут» - README переписан под новую грамматику
This commit is contained in:
@@ -6,44 +6,99 @@
|
||||
|
||||
```
|
||||
dev/harness/
|
||||
├── agents/ # Определения агентов OpenCode (leader, fixer, devops, admin, ...)
|
||||
├── agents/ # Определения агентов OpenCode (leader, fixer, devops, admin, ...)
|
||||
├── catalog/ # Скиллы: git submodule'ы на внешние репы
|
||||
│ ├── jakubkrehel-skills/ # → github.com/jakubkrehel/skills (11 скиллов UI)
|
||||
│ └── developer-documentation-skill/ # → NateBJones-Projects/… (доки; ставится вручную)
|
||||
├── scripts/
|
||||
│ └── harness # Основной CLI-скрипт
|
||||
│ └── harness # Основной CLI-скрипт
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Грамматика
|
||||
|
||||
**Объект — первым, действие — вторым:** `harness <объект> <действие> [аргументы]`
|
||||
|
||||
| Объект | Что это | Действия |
|
||||
|--------|---------|----------|
|
||||
| `status` | среда целиком | — |
|
||||
| `skills` | скиллы из каталога | `list`, `installed`, `install`, `remove` |
|
||||
| `agents` | агенты (только OpenCode) | `list`, `install`, `remove` |
|
||||
| `mcp` | MCP-серверы Hermes | `list`, `add`, `rm` |
|
||||
| `catalog` | внешние репозитории каталога | `list`, `update` |
|
||||
| `hermes` | сам Hermes | `config`, `restart`, `update` |
|
||||
| `opencode` | сам OpenCode | `install`, `restart` |
|
||||
|
||||
```bash
|
||||
./scripts/harness status
|
||||
./scripts/harness skills list
|
||||
./scripts/harness skills install jakubkrehel-skills better-ui --target all
|
||||
./scripts/harness agents install leader
|
||||
./scripts/harness mcp list
|
||||
./scripts/harness catalog update
|
||||
./scripts/harness hermes restart
|
||||
```
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
./scripts/harness status # Статус всей среды
|
||||
./scripts/harness list catalog # Что есть в каталоге скиллов
|
||||
./scripts/harness list skills # Установленные скиллы
|
||||
./scripts/harness list agents # Агенты: доступные в репо + установленные
|
||||
./scripts/harness list mcp # MCP-серверы Hermes
|
||||
./scripts/harness status # Обзор среды
|
||||
./scripts/harness skills list # Что доступно в каталоге (+ отметки установки)
|
||||
./scripts/harness skills installed # Что реально стоит у агентов
|
||||
./scripts/harness agents list # Агенты: доступные в репо + установленные
|
||||
./scripts/harness mcp list # MCP-серверы Hermes
|
||||
./scripts/harness catalog list # Источники каталога
|
||||
```
|
||||
|
||||
## Команды
|
||||
## `skills` — скиллы из каталога
|
||||
|
||||
### Установка скиллов
|
||||
### `skills list` — что доступно к установке
|
||||
|
||||
Показывает содержимое каталога и где скилл уже стоит. `✓` — установлено, `·` — нет.
|
||||
|
||||
```bash
|
||||
# Установить скилл из каталога
|
||||
./scripts/harness install skill jakubkrehel-skills better-ui --target opencode
|
||||
./scripts/harness install skill jakubkrehel-skills better-ui --target hermes
|
||||
./scripts/harness install skill jakubkrehel-skills better-ui --target all
|
||||
|
||||
# Можно писать через слэш
|
||||
./scripts/harness install skill jakubkrehel-skills/better-ui --target all
|
||||
|
||||
# Все скиллы из репозитория разом
|
||||
./scripts/harness install skill jakubkrehel-skills --all --target opencode
|
||||
./scripts/harness install skill jakubkrehel-skills --all --target all
|
||||
./scripts/harness skills list
|
||||
./scripts/harness skills list --target opencode
|
||||
```
|
||||
|
||||
Скиллы ставятся симлинками в соответствующие директории:
|
||||
```
|
||||
Доступно из каталога (/mnt/data/Agent/dev/harness/catalog)
|
||||
✓ — установлено, · — нет
|
||||
|
||||
jakubkrehel-skills 11 скиллов 267330e
|
||||
├─ better-ui hermes · opencode ✓
|
||||
├─ better-colors hermes · opencode ·
|
||||
...
|
||||
```
|
||||
|
||||
Для git-источников показывается короткий sha; `⚠ сдвинут` — submodule ушёл вперёд
|
||||
и это ещё не закоммичено.
|
||||
|
||||
### `skills installed` — что реально стоит
|
||||
|
||||
Полное дерево каталогов агента, включая скиллы не из каталога.
|
||||
|
||||
```bash
|
||||
./scripts/harness skills installed
|
||||
./scripts/harness skills installed --target hermes
|
||||
```
|
||||
|
||||
### `skills install` / `skills remove`
|
||||
|
||||
```bash
|
||||
# Один скилл
|
||||
./scripts/harness skills install jakubkrehel-skills better-ui --target opencode
|
||||
./scripts/harness skills install jakubkrehel-skills/better-ui --target all
|
||||
|
||||
# Все скиллы источника
|
||||
./scripts/harness skills install jakubkrehel-skills --all --target opencode
|
||||
|
||||
# Удалить
|
||||
./scripts/harness skills remove better-ui
|
||||
./scripts/harness skills remove better-ui --target hermes
|
||||
```
|
||||
|
||||
Скиллы ставятся **симлинками**:
|
||||
|
||||
| Агент | Куда ставится |
|
||||
|-------|--------------|
|
||||
@@ -51,33 +106,21 @@ dev/harness/
|
||||
| OpenCode | `~/.claude/skills/<name>/` |
|
||||
| OpenCode | `~/.config/opencode/skills/<name>/` |
|
||||
|
||||
### Управление скиллами
|
||||
|
||||
```bash
|
||||
./scripts/harness list skills # Все скиллы
|
||||
./scripts/harness list skills --target hermes # Только Hermes
|
||||
./scripts/harness remove skill better-ui # Удалить отовсюду
|
||||
./scripts/harness remove skill better-ui --target hermes
|
||||
```
|
||||
|
||||
### Агенты (только OpenCode)
|
||||
## `agents` — агенты (только OpenCode)
|
||||
|
||||
Источник — `harness/agents/*.md`. Имя агента = имя файла без `.md`.
|
||||
|
||||
```bash
|
||||
# Список: что есть в репо + что уже установлено
|
||||
./scripts/harness list agents
|
||||
./scripts/harness list agents --scope global # только глобальные
|
||||
./scripts/harness list agents --scope project --project ~/work/myrepo
|
||||
./scripts/harness agents list
|
||||
./scripts/harness agents list --scope global
|
||||
./scripts/harness agents list --scope project --project ~/work/myrepo
|
||||
|
||||
# Установить одного / всех
|
||||
./scripts/harness install agent leader # в ~/.config/opencode/agents/
|
||||
./scripts/harness install agent --all --scope global # всех, глобально
|
||||
./scripts/harness install agent devops --scope project --project ~/work/myrepo
|
||||
./scripts/harness agents install leader # в ~/.config/opencode/agents/
|
||||
./scripts/harness agents install --all --scope global
|
||||
./scripts/harness agents install devops --scope project --project ~/work/myrepo
|
||||
|
||||
# Удалить
|
||||
./scripts/harness remove agent leader # отовсюду
|
||||
./scripts/harness remove agent leader --scope global
|
||||
./scripts/harness agents remove leader
|
||||
./scripts/harness agents remove leader --scope global
|
||||
```
|
||||
|
||||
| `--scope` | Куда ставится |
|
||||
@@ -89,60 +132,47 @@ dev/harness/
|
||||
`mode: all` (кроме `leader` — `primary`), чтобы лидер мог вызывать их как сабагентов.
|
||||
|
||||
> **Hermes агентов не поддерживает** — только runtime-делегирование через
|
||||
> `delegate_task`. Пользовательских определений агентов у него нет, поэтому
|
||||
> `install agent` работает только для OpenCode.
|
||||
> `delegate_task`. Пользовательских определений агентов у него нет.
|
||||
|
||||
### MCP-серверы (Hermes)
|
||||
## `mcp` — MCP-серверы Hermes
|
||||
|
||||
```bash
|
||||
# Добавить MCP по URL (HTTP SSE)
|
||||
./scripts/harness configure hermes mcp add my-server http://localhost:8000/mcp
|
||||
./scripts/harness mcp list
|
||||
|
||||
# Добавить MCP через команду (STDIO)
|
||||
./scripts/harness configure hermes mcp add github npx \
|
||||
# По URL (HTTP/SSE)
|
||||
./scripts/harness mcp add my-server http://localhost:8000/mcp
|
||||
|
||||
# Командой (stdio)
|
||||
./scripts/harness mcp add github npx \
|
||||
--env GITHUB_TOKEN=ghp_xxx \
|
||||
--arg -y --arg @modelcontextprotocol/server-github
|
||||
|
||||
# Список MCP
|
||||
./scripts/harness list mcp
|
||||
|
||||
# Удалить MCP
|
||||
./scripts/harness configure hermes mcp rm my-server
|
||||
./scripts/harness mcp rm my-server
|
||||
```
|
||||
|
||||
### Конфигурация Hermes
|
||||
`add` и `rm` сами перезапускают `hermes-gateway`, чтобы изменения подхватились.
|
||||
|
||||
## `catalog` — внешние репозитории
|
||||
|
||||
```bash
|
||||
# Посмотреть конфиг
|
||||
./scripts/harness configure hermes
|
||||
|
||||
# Установить параметры
|
||||
./scripts/harness configure hermes --set model.default=gpt-4o
|
||||
./scripts/harness configure hermes --set model.provider=openai
|
||||
./scripts/harness configure hermes --set agent.max_turns=200
|
||||
./scripts/harness catalog list # источники, содержимое, sha, признак «сдвинут»
|
||||
./scripts/harness catalog update # git submodule update --remote / git pull для клонов
|
||||
```
|
||||
|
||||
После изменения конфига скрипт автоматически перезапускает Hermes.
|
||||
|
||||
### Установка/обновление бинарников
|
||||
## `hermes` / `opencode` — сам агент
|
||||
|
||||
```bash
|
||||
./scripts/harness install opencode # curl-установщик opencode.ai
|
||||
./scripts/harness install hermes # hermes update + рестарт сервисов
|
||||
./scripts/harness hermes config # показать конфиг
|
||||
./scripts/harness hermes config --set model.default=gpt-4o
|
||||
./scripts/harness hermes config --set agent.max_turns=200
|
||||
./scripts/harness hermes restart # systemctl --user restart hermes-gateway + webui
|
||||
./scripts/harness hermes update # hermes update + рестарт сервисов
|
||||
|
||||
./scripts/harness opencode install # curl-установщик opencode.ai
|
||||
./scripts/harness opencode restart # systemctl --user restart opencode + openchamber
|
||||
```
|
||||
|
||||
### Перезапуск
|
||||
|
||||
```bash
|
||||
./scripts/harness restart hermes # systemctl --user restart hermes-gateway + webui
|
||||
./scripts/harness restart opencode # systemctl --user restart opencode + openchamber
|
||||
```
|
||||
|
||||
### Обновление каталога
|
||||
|
||||
```bash
|
||||
./scripts/harness update catalog # git pull во всех репах внутри catalog/
|
||||
```
|
||||
`hermes config --set` меняет `~/.hermes/config.yaml` и перезапускает сервисы.
|
||||
|
||||
## Как добавить скилл в каталог
|
||||
|
||||
@@ -154,19 +184,16 @@ git submodule add https://github.com/user/skills-repo.git catalog/user-skills
|
||||
git commit -m "chore: add user-skills to catalog"
|
||||
```
|
||||
|
||||
После этого `harness list catalog` покажет все найденные скиллы, а
|
||||
`harness update catalog` подтянет свежие коммиты во всех submodule'ах.
|
||||
|
||||
Обычный клон (`git clone` внутрь `catalog/`) тоже работает — `update catalog`
|
||||
обновит его через `git pull`, но такой каталог не попадёт в коммит репы
|
||||
(вложенная репа не станет submodule автоматически).
|
||||
После этого `harness skills list` покажет найденные скиллы, а
|
||||
`harness catalog update` подтянет свежие коммиты во всех submodule'ах.
|
||||
|
||||
### Раскладка репозитория и ручная установка
|
||||
|
||||
CLI `harness` понимает одну раскладку — `<репа>/skills/<name>/SKILL.md`.
|
||||
Репозитории с другой структурой он показывает как `(no skills/)`, и их скиллы
|
||||
ставятся вручную. Ограничение только у CLI: сам OpenCode сканирует рекурсивно
|
||||
(`skills/**/SKILL.md`, а через `skills.paths` — `**/SKILL.md` на любой глубине).
|
||||
CLI понимает одну раскладку — `<репа>/skills/<name>/SKILL.md`. Репозитории с другой
|
||||
структурой (`SKILL.md` в корне, `.agents/skills/`, вложенные категории) он показывает
|
||||
без списка скиллов, и такие скиллы ставятся вручную. Ограничение только у CLI: сам
|
||||
OpenCode сканирует рекурсивно (`skills/**/SKILL.md`, а через `skills.paths` —
|
||||
`**/SKILL.md` на любой глубине).
|
||||
|
||||
Пример — `developer-documentation-skill` держит `SKILL.md` в корне репы:
|
||||
|
||||
@@ -186,7 +213,7 @@ ln -s /mnt/data/Agent/dev/harness/catalog/developer-documentation-skill \
|
||||
|
||||
## Как добавить агента
|
||||
|
||||
Положить `.md` файл в `agents/` репозитория — и он сразу виден через `harness list agents`.
|
||||
Положить `.md` файл в `agents/` репозитория — и он сразу виден через `harness agents list`.
|
||||
|
||||
Формат — как у OpenCode:
|
||||
|
||||
@@ -215,9 +242,7 @@ permission:
|
||||
## Поддерживаемые форматы
|
||||
|
||||
- **Скиллы** — `SKILL.md` с YAML frontmatter (`name`, `description`); родной формат OpenCode и Hermes.
|
||||
Скиллы из `catalog/*/skills/<name>/SKILL.md` видны через `harness list catalog`.
|
||||
- **Агенты** — `<name>.md` с frontmatter (`description`, `mode`, ...); формат OpenCode.
|
||||
Лежат в `agents/<name>.md`.
|
||||
|
||||
## Совместимость
|
||||
|
||||
|
||||
+347
-218
@@ -1,26 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Harness — универсальный менеджер скиллов, агентов, MCP-серверов и конфигов
|
||||
Harness — менеджер скиллов, агентов, MCP-серверов и конфигов
|
||||
для Hermes и OpenCode.
|
||||
|
||||
Usage:
|
||||
harness install skill <source> [--all] [--target opencode|hermes|all]
|
||||
harness install agent <name> [--all] [--scope global|project] [--project DIR]
|
||||
harness install opencode
|
||||
harness install hermes
|
||||
harness list skills [--target opencode|hermes]
|
||||
harness list agents [--scope global|project|all] [--project DIR]
|
||||
harness list mcp
|
||||
harness list catalog
|
||||
harness remove skill <name> [--target opencode|hermes|all]
|
||||
harness remove agent <name> [--scope global|project|all] [--project DIR]
|
||||
harness remove mcp <name>
|
||||
harness configure hermes [--set key=value ...]
|
||||
harness configure hermes mcp add <name> <url|command> [--env KEY=VAL] [--args ...]
|
||||
harness configure hermes mcp rm <name>
|
||||
harness restart <hermes|opencode>
|
||||
Грамматика: объект — первым, действие — вторым.
|
||||
|
||||
harness status
|
||||
harness update catalog
|
||||
harness skills list [--target hermes|opencode|all]
|
||||
harness skills installed [--target ...]
|
||||
harness skills install <source> [<skill>] [--all] [--target ...]
|
||||
harness skills remove <name> [--target ...]
|
||||
harness agents list [--scope global|project|all] [--project DIR]
|
||||
harness agents install <name>|--all [--scope ...] [--project DIR]
|
||||
harness agents remove <name> [--scope ...] [--project DIR]
|
||||
harness mcp list
|
||||
harness mcp add <name> <url|command> [--env KEY=VAL] [--arg ...]
|
||||
harness mcp rm <name>
|
||||
harness catalog list
|
||||
harness catalog update
|
||||
harness hermes config [--set key=value ...]
|
||||
harness hermes restart
|
||||
harness hermes update
|
||||
harness opencode install
|
||||
harness opencode restart
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -130,39 +132,27 @@ def read_frontmatter_field(path, field, width=None):
|
||||
# ─── Catalog ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def list_catalog():
|
||||
"""Scan catalog/ for skill sources (git repos with skills/ dirs)."""
|
||||
def catalog_entries():
|
||||
"""Источники каталога: [(name, dir, [skill_names])], отсортировано."""
|
||||
entries = []
|
||||
if not CATALOG_DIR.exists():
|
||||
print(" (empty — no catalog entries)")
|
||||
return
|
||||
for entry in sorted(CATALOG_DIR.iterdir()):
|
||||
if entry.is_dir():
|
||||
skills_dir = entry / "skills"
|
||||
if skills_dir.is_dir():
|
||||
skills = sorted(s.name for s in skills_dir.iterdir() if (s / "SKILL.md").exists())
|
||||
print(f" {c(entry.name, 'bold')} ({len(skills)} skills)")
|
||||
for s in skills:
|
||||
print(f" ├─ {s}")
|
||||
else:
|
||||
print(f" {c(entry.name, 'bold')} (no skills/)")
|
||||
# Check if it's just a git repo
|
||||
if (entry / ".git").exists():
|
||||
print(f" (git repo, no skills/ dir)")
|
||||
|
||||
|
||||
def catalog_skill_dirs():
|
||||
"""Yield (source_name, skill_name, skill_dir) for every skill in catalog."""
|
||||
if not CATALOG_DIR.exists():
|
||||
return
|
||||
return entries
|
||||
for entry in sorted(CATALOG_DIR.iterdir()):
|
||||
if not entry.is_dir():
|
||||
continue
|
||||
skills_dir = entry / "skills"
|
||||
if not skills_dir.is_dir():
|
||||
continue
|
||||
for skill_dir in skills_dir.iterdir():
|
||||
if (skill_dir / "SKILL.md").exists():
|
||||
yield entry.name, skill_dir.name, skill_dir
|
||||
names = []
|
||||
if skills_dir.is_dir():
|
||||
names = sorted(s.name for s in skills_dir.iterdir() if (s / "SKILL.md").exists())
|
||||
entries.append((entry.name, entry, names))
|
||||
return entries
|
||||
|
||||
|
||||
def catalog_skill_dirs():
|
||||
"""Yield (source_name, skill_name, skill_dir) for every skill in catalog."""
|
||||
for name, entry, skills in catalog_entries():
|
||||
for skill_name in skills:
|
||||
yield name, skill_name, entry / "skills" / skill_name
|
||||
|
||||
|
||||
def _submodule_paths():
|
||||
@@ -180,6 +170,54 @@ def _submodule_paths():
|
||||
return paths
|
||||
|
||||
|
||||
def source_git_state(entry):
|
||||
"""(short_sha, drift) для git-источника: sha и признак «сдвинут и не закоммичен»."""
|
||||
if not (entry / ".git").exists():
|
||||
return None, False
|
||||
rc, sha, _ = run(["git", "-C", str(entry), "rev-parse", "--short", "HEAD"])
|
||||
if rc != 0 or not sha:
|
||||
return None, False
|
||||
rel = str(entry.relative_to(HARNESS_ROOT))
|
||||
rc2, line, _ = run(["git", "ls-tree", "HEAD", rel], cwd=HARNESS_ROOT)
|
||||
drift = False
|
||||
if rc2 == 0 and line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 3 and parts[0] == "160000":
|
||||
drift = not parts[2].startswith(sha)
|
||||
return sha, drift
|
||||
|
||||
|
||||
def list_catalog():
|
||||
"""Источники каталога и их содержимое."""
|
||||
entries = catalog_entries()
|
||||
print(c("Каталог источников", "bold"), c(f"({CATALOG_DIR})", "dim"))
|
||||
if not entries:
|
||||
print(" (пусто)")
|
||||
return
|
||||
total = 0
|
||||
for name, entry, skills in entries:
|
||||
sha, drift = source_git_state(entry)
|
||||
tail = []
|
||||
if sha:
|
||||
tail.append(c(sha, "dim"))
|
||||
if drift:
|
||||
tail.append(c("⚠ сдвинут, не закоммичен", "yellow"))
|
||||
suffix = (" " + " ".join(tail)) if tail else ""
|
||||
if skills:
|
||||
print(f" {c(name, 'bold')} {c(str(len(skills)) + ' скиллов', 'dim')}{suffix}")
|
||||
for s in skills:
|
||||
print(f" ├─ {s}")
|
||||
total += len(skills)
|
||||
elif (entry / "SKILL.md").exists():
|
||||
nm = read_frontmatter_field(entry / "SKILL.md", "name") or name
|
||||
print(f" {c(name, 'bold')} {c('SKILL.md в корне репы', 'yellow')}{suffix}")
|
||||
print(f" ├─ {nm} {c('(ставится вручную)', 'dim')}")
|
||||
total += 1
|
||||
else:
|
||||
print(f" {c(name, 'bold')} {c('нет skills/', 'dim')}{suffix}")
|
||||
print(f"\n Всего скиллов: {c(str(total), 'cyan')}")
|
||||
|
||||
|
||||
def update_catalog():
|
||||
"""Обновить каталог: submodule'ы — через суперпроект, клоны — git pull."""
|
||||
if not CATALOG_DIR.exists():
|
||||
@@ -239,8 +277,8 @@ def install_skill(skill_source_name, skill_name, target):
|
||||
skill_dir = candidate
|
||||
|
||||
if not skill_dir:
|
||||
die(f"Skill '{skill_source_name}/{skill_name}' not found in catalog. "
|
||||
f"Use 'harness list catalog' to see available skills.")
|
||||
die(f"Скилл '{skill_source_name}/{skill_name}' не найден в каталоге. "
|
||||
f"Что есть — 'harness skills list'.")
|
||||
|
||||
targets = resolve_targets(target)
|
||||
|
||||
@@ -259,16 +297,16 @@ def install_all_skills(source_name, target):
|
||||
source_dir = CATALOG_DIR / source_name
|
||||
skills_dir = source_dir / "skills"
|
||||
if not skills_dir.is_dir():
|
||||
die(f"Source '{source_name}' not found in catalog, or has no skills/ directory")
|
||||
die(f"Источник '{source_name}' не найден в каталоге, или у него нет skills/")
|
||||
|
||||
skills = sorted(s.name for s in skills_dir.iterdir() if (s / "SKILL.md").exists())
|
||||
if not skills:
|
||||
die(f"No skills found in {source_name}/skills/")
|
||||
die(f"В {source_name}/skills/ нет скиллов")
|
||||
|
||||
info(f"Installing {len(skills)} skills from '{source_name}' → {target}")
|
||||
info(f"Устанавливаю {len(skills)} скиллов из '{source_name}' → {target}")
|
||||
for skill_name in skills:
|
||||
install_skill(source_name, skill_name, target)
|
||||
ok(f"All {len(skills)} skills installed")
|
||||
ok(f"Готово: {len(skills)} скиллов")
|
||||
|
||||
|
||||
def _remove_path(path):
|
||||
@@ -284,7 +322,7 @@ def _install_skill_dir(src_dir, dest_dir, agent_name):
|
||||
dest_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if dest_dir.exists():
|
||||
warn(f" [{agent_name}] {dest_dir.name} already exists, removing first")
|
||||
warn(f" [{agent_name}] {dest_dir.name} уже есть — заменяю")
|
||||
_remove_path(dest_dir)
|
||||
|
||||
try:
|
||||
@@ -293,35 +331,126 @@ def _install_skill_dir(src_dir, dest_dir, agent_name):
|
||||
except OSError:
|
||||
# Fallback to copy
|
||||
shutil.copytree(src_dir, dest_dir, symlinks=True)
|
||||
ok(f" [{agent_name}] → {dest_dir} (copied)")
|
||||
ok(f" [{agent_name}] → {dest_dir} (скопирован)")
|
||||
|
||||
|
||||
def remove_skill(skill_name, target):
|
||||
"""Remove a skill from target agent."""
|
||||
targets = resolve_targets(target)
|
||||
removed = 0
|
||||
for t in targets:
|
||||
if t == "hermes":
|
||||
paths = list(HERMES_SKILLS.glob(f"*/{skill_name}")) + list(HERMES_SKILLS.glob(f"**/{skill_name}"))
|
||||
for p in paths:
|
||||
if p.exists():
|
||||
_remove_path(p)
|
||||
ok(f" [hermes] removed {p}")
|
||||
ok(f" [hermes] удалён {p}")
|
||||
removed += 1
|
||||
elif t == "opencode":
|
||||
for skill_root in OPENCODE_SKILLS_DIRS:
|
||||
dest = skill_root / skill_name
|
||||
if dest.exists():
|
||||
_remove_path(dest)
|
||||
ok(f" [opencode] removed {dest}")
|
||||
ok(f" [opencode] удалён {dest}")
|
||||
removed += 1
|
||||
if not removed:
|
||||
warn(f"Скилл '{skill_name}' не найден у {', '.join(targets)}")
|
||||
|
||||
|
||||
def list_skills(target):
|
||||
"""List installed skills."""
|
||||
def _skill_dirs(roots):
|
||||
"""Yield каталоги с SKILL.md. Симлинки разворачиваются (os.walk followlinks)."""
|
||||
for root in roots:
|
||||
if not root.exists():
|
||||
continue
|
||||
seen = set()
|
||||
for dirpath, dirnames, filenames in os.walk(root, followlinks=True):
|
||||
try:
|
||||
st = os.stat(dirpath)
|
||||
except OSError:
|
||||
dirnames[:] = []
|
||||
continue
|
||||
key = (st.st_dev, st.st_ino)
|
||||
if key in seen:
|
||||
dirnames[:] = []
|
||||
continue
|
||||
seen.add(key)
|
||||
if "SKILL.md" in filenames:
|
||||
yield Path(dirpath)
|
||||
|
||||
|
||||
def _skill_roots(target):
|
||||
return [HERMES_SKILLS] if target == "hermes" else OPENCODE_SKILLS_DIRS
|
||||
|
||||
|
||||
def installed_skill_names(target):
|
||||
"""Имена скиллов, лежащих в каталогах агента (включая симлинки)."""
|
||||
return {p.name for p in _skill_dirs(_skill_roots(target))}
|
||||
|
||||
|
||||
def count_skills(target):
|
||||
"""Сколько уникальных скиллов установлено у агента."""
|
||||
return len(installed_skill_names(target))
|
||||
|
||||
|
||||
def list_skills(target="all"):
|
||||
"""Каталог: что доступно к установке/обновлению, и где уже стоит."""
|
||||
targets = resolve_targets(target)
|
||||
installed = {t: installed_skill_names(t) for t in targets}
|
||||
|
||||
print(c("Доступно из каталога", "bold"), c(f"({CATALOG_DIR})", "dim"))
|
||||
print(c(" ✓ — установлено, · — нет", "dim"))
|
||||
|
||||
entries = catalog_entries()
|
||||
if not entries:
|
||||
print(" (каталог пуст)")
|
||||
return
|
||||
|
||||
total = 0
|
||||
have = 0
|
||||
for name, entry, skills in entries:
|
||||
sha, drift = source_git_state(entry)
|
||||
tail = []
|
||||
if sha:
|
||||
tail.append(c(sha, "dim"))
|
||||
if drift:
|
||||
tail.append(c("⚠ сдвинут", "yellow"))
|
||||
suffix = (" " + " ".join(tail)) if tail else ""
|
||||
|
||||
if skills:
|
||||
print(f"\n {c(name, 'bold')} {c(str(len(skills)) + ' скиллов', 'dim')}{suffix}")
|
||||
for s in skills:
|
||||
marks = " ".join(
|
||||
c(f"{t} ✓", "green") if s in installed[t] else c(f"{t} ·", "dim")
|
||||
for t in targets
|
||||
)
|
||||
print(f" ├─ {c(s.ljust(28), 'cyan')} {marks}")
|
||||
total += 1
|
||||
if any(s in installed[t] for t in targets):
|
||||
have += 1
|
||||
elif (entry / "SKILL.md").exists():
|
||||
nm = read_frontmatter_field(entry / "SKILL.md", "name") or name
|
||||
print(f"\n {c(name, 'bold')} {c('SKILL.md в корне репы', 'yellow')}{suffix}")
|
||||
print(f" ├─ {c(nm.ljust(28), 'cyan')} "
|
||||
f"{c('ставится вручную', 'dim')}")
|
||||
total += 1
|
||||
if nm in installed.get("opencode", set()):
|
||||
have += 1
|
||||
else:
|
||||
print(f"\n {c(name, 'bold')} {c('нет skills/', 'dim')}{suffix}")
|
||||
|
||||
print(f"\n В каталоге: {c(str(total), 'cyan')} установлено: {c(str(have), 'cyan')}")
|
||||
if have < total:
|
||||
print(c(f" Поставить: harness skills install <источник> <скилл>|--all", "dim"))
|
||||
|
||||
|
||||
def list_skills_installed(target="all"):
|
||||
"""Что реально установлено у агентов (включая не из каталога)."""
|
||||
targets = resolve_targets(target)
|
||||
for t in targets:
|
||||
if t == "hermes":
|
||||
print(f"\n{c('Hermes skills', 'bold')} ({HERMES_SKILLS}):")
|
||||
_print_skill_tree(HERMES_SKILLS)
|
||||
elif t == "opencode":
|
||||
else:
|
||||
for skill_root in OPENCODE_SKILLS_DIRS:
|
||||
if skill_root.exists():
|
||||
print(f"\n{c('OpenCode skills', 'bold')} ({skill_root}):")
|
||||
@@ -586,7 +715,7 @@ def _parse_value(value):
|
||||
return value
|
||||
|
||||
|
||||
# ─── Install Agents Binaries ──────────────────────────────────────
|
||||
# ─── Binaries ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def install_opencode():
|
||||
@@ -602,12 +731,11 @@ def install_opencode():
|
||||
die(f"OpenCode install failed: {err}")
|
||||
|
||||
|
||||
def install_hermes():
|
||||
def update_hermes():
|
||||
info("Updating Hermes...")
|
||||
rc, out, err = run(["hermes", "update"], timeout=120)
|
||||
if rc == 0:
|
||||
ok("Hermes updated")
|
||||
# Restart services
|
||||
restart_hermes()
|
||||
else:
|
||||
warn(f"hermes update failed: {err}")
|
||||
@@ -667,8 +795,7 @@ def show_status():
|
||||
enabled = sum(1 for s in servers.values() if s.get("enabled", True))
|
||||
print(f" MCP servers: {c(str(len(servers)), 'cyan')} ({enabled} enabled)")
|
||||
|
||||
skill_count = sum(1 for _ in HERMES_SKILLS.rglob("SKILL.md")) if HERMES_SKILLS.exists() else 0
|
||||
print(f" Skills: {c(str(skill_count), 'cyan')}")
|
||||
print(f" Skills: {c(str(count_skills('hermes')), 'cyan')}")
|
||||
|
||||
# OpenCode
|
||||
print(f"\n{c('OpenCode', 'bold')}")
|
||||
@@ -692,11 +819,7 @@ def show_status():
|
||||
for svc in OPENCODE_SERVICES:
|
||||
_service_line(svc)
|
||||
|
||||
oc_skills = 0
|
||||
for d in OPENCODE_SKILLS_DIRS:
|
||||
if d.exists():
|
||||
oc_skills += sum(1 for _ in d.rglob("SKILL.md"))
|
||||
print(f" Skills: {c(str(oc_skills), 'cyan')}")
|
||||
print(f" Skills: {c(str(count_skills('opencode')), 'cyan')}")
|
||||
|
||||
oc_agents = len(list(OPENCODE_AGENTS_GLOBAL.glob("*.md"))) if OPENCODE_AGENTS_GLOBAL.exists() else 0
|
||||
repo_agents = len(available_agents())
|
||||
@@ -704,13 +827,13 @@ def show_status():
|
||||
|
||||
# Catalog
|
||||
print(f"\n{c('Catalog', 'bold')}")
|
||||
if CATALOG_DIR.exists():
|
||||
entries = [d for d in CATALOG_DIR.iterdir() if d.is_dir()]
|
||||
entries = catalog_entries()
|
||||
if entries:
|
||||
print(f" Sources: {c(str(len(entries)), 'cyan')}")
|
||||
for e in entries:
|
||||
skills_dir = e / "skills"
|
||||
n = len(list(skills_dir.glob("*/SKILL.md"))) if skills_dir.exists() else 0
|
||||
print(f" ├─ {c(e.name, 'cyan')} ({n} skills)")
|
||||
for name, entry, skills in entries:
|
||||
sha, drift = source_git_state(entry)
|
||||
mark = c(" ⚠ сдвинут", "yellow") if drift else ""
|
||||
print(f" ├─ {c(name, 'cyan')} ({len(skills)} skills){mark}")
|
||||
else:
|
||||
print(f" {c('Not found', 'red')}")
|
||||
|
||||
@@ -722,190 +845,196 @@ def show_status():
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Harness — Manager for Hermes & OpenCode skills, agents, MCP, and configs",
|
||||
description="Harness — менеджер скиллов, агентов, MCP и конфигов для Hermes и OpenCode",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
Грамматика: объект — первым, действие — вторым.
|
||||
|
||||
Примеры:
|
||||
harness status
|
||||
harness list catalog
|
||||
harness list agents
|
||||
harness install skill jakubkrehel-skills better-ui --target all
|
||||
harness install skill jakubkrehel-skills --all --target opencode
|
||||
harness install agent leader --scope global
|
||||
harness install agent --all --scope project --project ~/work/myrepo
|
||||
harness install opencode
|
||||
harness configure hermes --set model.default=gpt-4o
|
||||
harness configure hermes mcp add my-server http://localhost:8000/mcp
|
||||
harness configure hermes mcp add github npx --env GITHUB_TOKEN=xxx --arg -y --arg @modelcontextprotocol/server-github
|
||||
harness remove skill better-ui --target hermes
|
||||
harness remove agent leader
|
||||
harness update catalog
|
||||
|
||||
harness skills list
|
||||
harness skills installed
|
||||
harness skills install jakubkrehel-skills better-ui --target all
|
||||
harness skills install jakubkrehel-skills --all --target opencode
|
||||
harness skills remove better-ui --target hermes
|
||||
|
||||
harness agents list
|
||||
harness agents install leader --scope global
|
||||
harness agents install --all --scope project --project ~/work/myrepo
|
||||
harness agents remove leader
|
||||
|
||||
harness mcp list
|
||||
harness mcp add my-server http://localhost:8000/mcp
|
||||
harness mcp add github npx --env GITHUB_TOKEN=xxx --arg -y --arg @modelcontextprotocol/server-github
|
||||
harness mcp rm my-server
|
||||
|
||||
harness catalog list
|
||||
harness catalog update
|
||||
|
||||
harness hermes config --set model.default=gpt-4o
|
||||
harness hermes restart
|
||||
harness hermes update
|
||||
|
||||
harness opencode install
|
||||
harness opencode restart
|
||||
""",
|
||||
)
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
|
||||
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
sub = parser.add_subparsers(dest="object", required=True)
|
||||
|
||||
# install
|
||||
install_parser = sub.add_parser("install", help="Install things")
|
||||
install_sub = install_parser.add_subparsers(dest="install_type", required=True)
|
||||
# ── status
|
||||
sub.add_parser("status", help="Обзор среды (сервисы, версии, счётчики)")
|
||||
|
||||
# install skill
|
||||
skill_parser = install_sub.add_parser("skill")
|
||||
skill_parser.add_argument("source", help="Catalog source name (e.g. jakubkrehel-skills)")
|
||||
skill_parser.add_argument("skill", nargs="?", default=None, help="Skill name (or omit with --all)")
|
||||
skill_parser.add_argument("--target", default="all", choices=["hermes", "opencode", "all"])
|
||||
skill_parser.add_argument("--all", action="store_true", help="Install all skills from the source")
|
||||
# ── skills
|
||||
skills_p = sub.add_parser("skills", help="Скиллы из каталога")
|
||||
skills_sub = skills_p.add_subparsers(dest="action", required=True, metavar="<действие>")
|
||||
|
||||
# install agent (OpenCode only)
|
||||
agent_parser = install_sub.add_parser("agent", help="Install OpenCode agent definition(s)")
|
||||
agent_parser.add_argument("name", nargs="?", default=None, help="Agent name (file name without .md)")
|
||||
agent_parser.add_argument("--all", action="store_true", help="Install all agents")
|
||||
agent_parser.add_argument("--scope", default="global", choices=["global", "project", "all"],
|
||||
help="global → ~/.config/opencode/agents; project → <project>/.opencode/agents")
|
||||
agent_parser.add_argument("--project", default=None, help="Project root (default: cwd)")
|
||||
sk_list = skills_sub.add_parser("list", help="Что доступно в каталоге (+ где уже стоит)")
|
||||
sk_list.add_argument("--target", default="all", choices=["hermes", "opencode", "all"])
|
||||
|
||||
# install opencode / hermes
|
||||
install_sub.add_parser("opencode", help="Install/update OpenCode binary")
|
||||
install_sub.add_parser("hermes", help="Update Hermes")
|
||||
sk_inst = skills_sub.add_parser("installed", help="Что реально установлено у агентов")
|
||||
sk_inst.add_argument("--target", default="all", choices=["hermes", "opencode", "all"])
|
||||
|
||||
# list
|
||||
list_parser = sub.add_parser("list", help="List installed items")
|
||||
list_sub = list_parser.add_subparsers(dest="list_type", required=True)
|
||||
list_sub.add_parser("skills", help="List installed skills").add_argument(
|
||||
"--target", default="all", choices=["hermes", "opencode", "all"]
|
||||
)
|
||||
list_agents_p = list_sub.add_parser("agents", help="List OpenCode agents")
|
||||
list_agents_p.add_argument("--scope", default="all", choices=["global", "project", "all"])
|
||||
list_agents_p.add_argument("--project", default=None, help="Project root (default: cwd)")
|
||||
list_sub.add_parser("mcp", help="List MCP servers")
|
||||
list_sub.add_parser("catalog", help="List catalog entries")
|
||||
sk_add = skills_sub.add_parser("install", help="Установить скилл(ы) из каталога")
|
||||
sk_add.add_argument("source", help="Источник в каталоге (напр. jakubkrehel-skills)")
|
||||
sk_add.add_argument("skill", nargs="?", default=None, help="Имя скилла (или --all)")
|
||||
sk_add.add_argument("--all", action="store_true", help="Все скиллы источника")
|
||||
sk_add.add_argument("--target", default="all", choices=["hermes", "opencode", "all"])
|
||||
|
||||
# remove
|
||||
remove_parser = sub.add_parser("remove", help="Remove items")
|
||||
remove_sub = remove_parser.add_subparsers(dest="remove_type", required=True)
|
||||
rm_skill = remove_sub.add_parser("skill")
|
||||
rm_skill.add_argument("name", help="Skill name to remove")
|
||||
rm_skill.add_argument("--target", default="all", choices=["hermes", "opencode", "all"])
|
||||
rm_agent = remove_sub.add_parser("agent")
|
||||
rm_agent.add_argument("name", help="Agent name to remove")
|
||||
rm_agent.add_argument("--scope", default="all", choices=["global", "project", "all"])
|
||||
rm_agent.add_argument("--project", default=None, help="Project root (default: cwd)")
|
||||
rm_mcp = remove_sub.add_parser("mcp")
|
||||
rm_mcp.add_argument("name", help="MCP server name to remove")
|
||||
sk_rm = skills_sub.add_parser("remove", help="Удалить скилл у агентов")
|
||||
sk_rm.add_argument("name", help="Имя скилла")
|
||||
sk_rm.add_argument("--target", default="all", choices=["hermes", "opencode", "all"])
|
||||
|
||||
# configure
|
||||
configure_parser = sub.add_parser("configure", help="Configure agents")
|
||||
configure_sub = configure_parser.add_subparsers(dest="configure_type", required=True)
|
||||
hermes_conf = configure_sub.add_parser("hermes", help="Configure Hermes")
|
||||
hermes_conf_sub = hermes_conf.add_subparsers(dest="hermes_action")
|
||||
# ── agents
|
||||
agents_p = sub.add_parser("agents", help="Агенты (только OpenCode)")
|
||||
agents_sub = agents_p.add_subparsers(dest="action", required=True, metavar="<действие>")
|
||||
|
||||
# configure hermes (plain — show or set)
|
||||
hermes_conf.add_argument("--set", "-s", action="append", default=[],
|
||||
help="key=value pairs (repeatable). Omit to show config.")
|
||||
ag_list = agents_sub.add_parser("list", help="Доступные в репо и установленные агенты")
|
||||
ag_list.add_argument("--scope", default="all", choices=["global", "project", "all"])
|
||||
ag_list.add_argument("--project", default=None, help="Корень проекта (по умолчанию cwd)")
|
||||
|
||||
# configure hermes mcp
|
||||
mcp_parser = hermes_conf_sub.add_parser("mcp", help="Manage Hermes MCP servers")
|
||||
mcp_sub = mcp_parser.add_subparsers(dest="mcp_action", required=True)
|
||||
mcp_add = mcp_sub.add_parser("add")
|
||||
mcp_add.add_argument("name")
|
||||
mcp_add.add_argument("source", help="URL or command")
|
||||
mcp_add.add_argument("--env", action="append", default=[], help="ENV=VAR for command servers")
|
||||
mcp_add.add_argument("--args", nargs="*", default=[], help="Extra args (e.g. -y @package)")
|
||||
mcp_add.add_argument("--arg", "-a", dest="args_list", action="append", default=[],
|
||||
help="Extra arg (repeatable, for npx packages)")
|
||||
ag_add = agents_sub.add_parser("install", help="Установить агента(ов)")
|
||||
ag_add.add_argument("name", nargs="?", default=None, help="Имя агента (или --all)")
|
||||
ag_add.add_argument("--all", action="store_true", help="Все агенты")
|
||||
ag_add.add_argument("--scope", default="global", choices=["global", "project", "all"],
|
||||
help="global → ~/.config/opencode/agents; project → <project>/.opencode/agents")
|
||||
ag_add.add_argument("--project", default=None, help="Корень проекта (по умолчанию cwd)")
|
||||
|
||||
mcp_rm = mcp_sub.add_parser("rm")
|
||||
mcp_rm.add_argument("name", help="MCP server name to remove")
|
||||
ag_rm = agents_sub.add_parser("remove", help="Удалить агента")
|
||||
ag_rm.add_argument("name", help="Имя агента")
|
||||
ag_rm.add_argument("--scope", default="all", choices=["global", "project", "all"])
|
||||
ag_rm.add_argument("--project", default=None, help="Корень проекта (по умолчанию cwd)")
|
||||
|
||||
# restart
|
||||
restart_parser = sub.add_parser("restart", help="Restart agents")
|
||||
restart_parser.add_argument("target", choices=["hermes", "opencode"], help="Agent to restart")
|
||||
# ── mcp
|
||||
mcp_p = sub.add_parser("mcp", help="MCP-серверы Hermes")
|
||||
mcp_sub = mcp_p.add_subparsers(dest="action", required=True, metavar="<действие>")
|
||||
|
||||
# status
|
||||
sub.add_parser("status", help="Show status of everything")
|
||||
mcp_sub.add_parser("list", help="Список MCP-серверов")
|
||||
|
||||
# update
|
||||
sub.add_parser("update", help="Update catalog").add_argument("type", nargs="?", default="catalog")
|
||||
mcp_add_p = mcp_sub.add_parser("add", help="Добавить MCP-сервер (+ рестарт gateway)")
|
||||
mcp_add_p.add_argument("name")
|
||||
mcp_add_p.add_argument("source", help="URL или команда")
|
||||
mcp_add_p.add_argument("--env", action="append", default=[], help="ENV=VAR (для stdio)")
|
||||
mcp_add_p.add_argument("--args", nargs="*", default=[], help="Доп. аргументы")
|
||||
mcp_add_p.add_argument("--arg", "-a", dest="args_list", action="append", default=[],
|
||||
help="Доп. аргумент (повторяемый)")
|
||||
|
||||
mcp_rm_p = mcp_sub.add_parser("rm", help="Удалить MCP-сервер (+ рестарт gateway)")
|
||||
mcp_rm_p.add_argument("name", help="Имя MCP-сервера")
|
||||
|
||||
# ── catalog
|
||||
cat_p = sub.add_parser("catalog", help="Каталог внешних репозиториев")
|
||||
cat_sub = cat_p.add_subparsers(dest="action", required=True, metavar="<действие>")
|
||||
cat_sub.add_parser("list", help="Источники каталога и их содержимое")
|
||||
cat_sub.add_parser("update", help="Обновить submodule'ы / клоны")
|
||||
|
||||
# ── hermes
|
||||
hermes_p = sub.add_parser("hermes", help="Hermes: конфиг и сервисы")
|
||||
hermes_sub = hermes_p.add_subparsers(dest="action", required=True, metavar="<действие>")
|
||||
h_conf = hermes_sub.add_parser("config", help="Показать или изменить конфиг")
|
||||
h_conf.add_argument("--set", "-s", action="append", default=[],
|
||||
help="key=value (повторяемо). Без него — показать конфиг.")
|
||||
hermes_sub.add_parser("restart", help="Перезапустить gateway + webui")
|
||||
hermes_sub.add_parser("update", help="hermes update + рестарт сервисов")
|
||||
|
||||
# ── opencode
|
||||
oc_p = sub.add_parser("opencode", help="OpenCode: бинарник и сервисы")
|
||||
oc_sub = oc_p.add_subparsers(dest="action", required=True, metavar="<действие>")
|
||||
oc_sub.add_parser("install", help="Установить/обновить OpenCode")
|
||||
oc_sub.add_parser("restart", help="Перезапустить opencode + openchamber")
|
||||
|
||||
args = parser.parse_args()
|
||||
os.makedirs(CATALOG_DIR, exist_ok=True)
|
||||
|
||||
# ── Commands ──────────────────────────────────────────────
|
||||
# ── Dispatch ──────────────────────────────────────────────
|
||||
|
||||
if args.command == "install":
|
||||
if args.install_type == "skill":
|
||||
source = args.source
|
||||
skill = args.skill
|
||||
obj = args.object
|
||||
action = getattr(args, "action", None)
|
||||
|
||||
if obj == "status":
|
||||
show_status()
|
||||
|
||||
elif obj == "skills":
|
||||
if action == "list":
|
||||
list_skills(args.target)
|
||||
elif action == "installed":
|
||||
list_skills_installed(args.target)
|
||||
elif action == "install":
|
||||
source, skill = args.source, args.skill
|
||||
if "/" in source and not skill:
|
||||
source, skill = source.split("/", 1)
|
||||
|
||||
if args.all:
|
||||
install_all_skills(source, args.target)
|
||||
elif skill is None:
|
||||
die("Specify skill name: harness install skill <source> <skill> or use --all")
|
||||
die("Укажи имя скилла или --all: harness skills install <источник> <скилл>")
|
||||
else:
|
||||
install_skill(source, skill, args.target)
|
||||
|
||||
elif args.install_type == "agent":
|
||||
install_agent(args.name, args.all, args.scope, args.project)
|
||||
|
||||
elif args.install_type == "opencode":
|
||||
install_opencode()
|
||||
|
||||
elif args.install_type == "hermes":
|
||||
install_hermes()
|
||||
|
||||
elif args.command == "list":
|
||||
if args.list_type == "skills":
|
||||
list_skills(args.target)
|
||||
elif args.list_type == "agents":
|
||||
list_agents(args.scope, args.project)
|
||||
elif args.list_type == "mcp":
|
||||
list_mcp()
|
||||
elif args.list_type == "catalog":
|
||||
list_catalog()
|
||||
|
||||
elif args.command == "remove":
|
||||
if args.remove_type == "skill":
|
||||
elif action == "remove":
|
||||
remove_skill(args.name, args.target)
|
||||
elif args.remove_type == "agent":
|
||||
|
||||
elif obj == "agents":
|
||||
if action == "list":
|
||||
list_agents(args.scope, args.project)
|
||||
elif action == "install":
|
||||
install_agent(args.name, args.all, args.scope, args.project)
|
||||
elif action == "remove":
|
||||
remove_agent(args.name, args.scope, args.project)
|
||||
elif args.remove_type == "mcp":
|
||||
|
||||
elif obj == "mcp":
|
||||
if action == "list":
|
||||
list_mcp()
|
||||
elif action == "add":
|
||||
env_dict = {}
|
||||
for e in args.env:
|
||||
if "=" in e:
|
||||
k, v = e.split("=", 1)
|
||||
env_dict[k] = v
|
||||
mcp_add(args.name, args.source, env_vars=env_dict, args=args.args + args.args_list)
|
||||
elif action == "rm":
|
||||
mcp_rm(args.name)
|
||||
|
||||
elif args.command == "configure":
|
||||
if args.configure_type == "hermes":
|
||||
if hasattr(args, "hermes_action") and args.hermes_action == "mcp":
|
||||
if args.mcp_action == "add":
|
||||
# Combine --args and --arg
|
||||
all_args = args.args + args.args_list
|
||||
env_dict = {}
|
||||
for e in args.env:
|
||||
if "=" in e:
|
||||
k, v = e.split("=", 1)
|
||||
env_dict[k] = v
|
||||
mcp_add(args.name, args.source, env_vars=env_dict, args=all_args)
|
||||
elif args.mcp_action == "rm":
|
||||
mcp_rm(args.name)
|
||||
else:
|
||||
configure_hermes(args.set if hasattr(args, "set") else None)
|
||||
else:
|
||||
die(f"Configure target not supported: {args.configure_type}")
|
||||
elif obj == "catalog":
|
||||
if action == "list":
|
||||
list_catalog()
|
||||
elif action == "update":
|
||||
update_catalog()
|
||||
|
||||
elif args.command == "restart":
|
||||
if args.target == "hermes":
|
||||
elif obj == "hermes":
|
||||
if action == "config":
|
||||
configure_hermes(args.set)
|
||||
elif action == "restart":
|
||||
restart_hermes()
|
||||
else:
|
||||
elif action == "update":
|
||||
update_hermes()
|
||||
|
||||
elif obj == "opencode":
|
||||
if action == "install":
|
||||
install_opencode()
|
||||
elif action == "restart":
|
||||
restart_opencode()
|
||||
|
||||
elif args.command == "status":
|
||||
show_status()
|
||||
|
||||
elif args.command == "update":
|
||||
update_catalog()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user