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/
|
dev/harness/
|
||||||
├── agents/ # Определения агентов OpenCode (leader, fixer, devops, admin, ...)
|
├── agents/ # Определения агентов OpenCode (leader, fixer, devops, admin, ...)
|
||||||
├── catalog/ # Скиллы: git submodule'ы на внешние репы
|
├── catalog/ # Скиллы: git submodule'ы на внешние репы
|
||||||
│ ├── jakubkrehel-skills/ # → github.com/jakubkrehel/skills (11 скиллов UI)
|
│ ├── jakubkrehel-skills/ # → github.com/jakubkrehel/skills (11 скиллов UI)
|
||||||
│ └── developer-documentation-skill/ # → NateBJones-Projects/… (доки; ставится вручную)
|
│ └── developer-documentation-skill/ # → NateBJones-Projects/… (доки; ставится вручную)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ └── harness # Основной CLI-скрипт
|
│ └── harness # Основной CLI-скрипт
|
||||||
└── README.md
|
└── 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
|
```bash
|
||||||
./scripts/harness status # Статус всей среды
|
./scripts/harness status # Обзор среды
|
||||||
./scripts/harness list catalog # Что есть в каталоге скиллов
|
./scripts/harness skills list # Что доступно в каталоге (+ отметки установки)
|
||||||
./scripts/harness list skills # Установленные скиллы
|
./scripts/harness skills installed # Что реально стоит у агентов
|
||||||
./scripts/harness list agents # Агенты: доступные в репо + установленные
|
./scripts/harness agents list # Агенты: доступные в репо + установленные
|
||||||
./scripts/harness list mcp # MCP-серверы Hermes
|
./scripts/harness mcp list # MCP-серверы Hermes
|
||||||
|
./scripts/harness catalog list # Источники каталога
|
||||||
```
|
```
|
||||||
|
|
||||||
## Команды
|
## `skills` — скиллы из каталога
|
||||||
|
|
||||||
### Установка скиллов
|
### `skills list` — что доступно к установке
|
||||||
|
|
||||||
|
Показывает содержимое каталога и где скилл уже стоит. `✓` — установлено, `·` — нет.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Установить скилл из каталога
|
./scripts/harness skills list
|
||||||
./scripts/harness install skill jakubkrehel-skills better-ui --target opencode
|
./scripts/harness skills list --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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Скиллы ставятся симлинками в соответствующие директории:
|
```
|
||||||
|
Доступно из каталога (/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 | `~/.claude/skills/<name>/` |
|
||||||
| OpenCode | `~/.config/opencode/skills/<name>/` |
|
| OpenCode | `~/.config/opencode/skills/<name>/` |
|
||||||
|
|
||||||
### Управление скиллами
|
## `agents` — агенты (только OpenCode)
|
||||||
|
|
||||||
```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)
|
|
||||||
|
|
||||||
Источник — `harness/agents/*.md`. Имя агента = имя файла без `.md`.
|
Источник — `harness/agents/*.md`. Имя агента = имя файла без `.md`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Список: что есть в репо + что уже установлено
|
./scripts/harness agents list
|
||||||
./scripts/harness list agents
|
./scripts/harness agents list --scope global
|
||||||
./scripts/harness list agents --scope global # только глобальные
|
./scripts/harness agents list --scope project --project ~/work/myrepo
|
||||||
./scripts/harness list agents --scope project --project ~/work/myrepo
|
|
||||||
|
|
||||||
# Установить одного / всех
|
./scripts/harness agents install leader # в ~/.config/opencode/agents/
|
||||||
./scripts/harness install agent leader # в ~/.config/opencode/agents/
|
./scripts/harness agents install --all --scope global
|
||||||
./scripts/harness install agent --all --scope global # всех, глобально
|
./scripts/harness agents install devops --scope project --project ~/work/myrepo
|
||||||
./scripts/harness install agent devops --scope project --project ~/work/myrepo
|
|
||||||
|
|
||||||
# Удалить
|
./scripts/harness agents remove leader
|
||||||
./scripts/harness remove agent leader # отовсюду
|
./scripts/harness agents remove leader --scope global
|
||||||
./scripts/harness remove agent leader --scope global
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| `--scope` | Куда ставится |
|
| `--scope` | Куда ставится |
|
||||||
@@ -89,60 +132,47 @@ dev/harness/
|
|||||||
`mode: all` (кроме `leader` — `primary`), чтобы лидер мог вызывать их как сабагентов.
|
`mode: all` (кроме `leader` — `primary`), чтобы лидер мог вызывать их как сабагентов.
|
||||||
|
|
||||||
> **Hermes агентов не поддерживает** — только runtime-делегирование через
|
> **Hermes агентов не поддерживает** — только runtime-делегирование через
|
||||||
> `delegate_task`. Пользовательских определений агентов у него нет, поэтому
|
> `delegate_task`. Пользовательских определений агентов у него нет.
|
||||||
> `install agent` работает только для OpenCode.
|
|
||||||
|
|
||||||
### MCP-серверы (Hermes)
|
## `mcp` — MCP-серверы Hermes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Добавить MCP по URL (HTTP SSE)
|
./scripts/harness mcp list
|
||||||
./scripts/harness configure hermes mcp add my-server http://localhost:8000/mcp
|
|
||||||
|
|
||||||
# Добавить MCP через команду (STDIO)
|
# По URL (HTTP/SSE)
|
||||||
./scripts/harness configure hermes mcp add github npx \
|
./scripts/harness mcp add my-server http://localhost:8000/mcp
|
||||||
|
|
||||||
|
# Командой (stdio)
|
||||||
|
./scripts/harness mcp add github npx \
|
||||||
--env GITHUB_TOKEN=ghp_xxx \
|
--env GITHUB_TOKEN=ghp_xxx \
|
||||||
--arg -y --arg @modelcontextprotocol/server-github
|
--arg -y --arg @modelcontextprotocol/server-github
|
||||||
|
|
||||||
# Список MCP
|
./scripts/harness mcp rm my-server
|
||||||
./scripts/harness list mcp
|
|
||||||
|
|
||||||
# Удалить MCP
|
|
||||||
./scripts/harness configure hermes mcp rm my-server
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Конфигурация Hermes
|
`add` и `rm` сами перезапускают `hermes-gateway`, чтобы изменения подхватились.
|
||||||
|
|
||||||
|
## `catalog` — внешние репозитории
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Посмотреть конфиг
|
./scripts/harness catalog list # источники, содержимое, sha, признак «сдвинут»
|
||||||
./scripts/harness configure hermes
|
./scripts/harness catalog update # git submodule update --remote / git pull для клонов
|
||||||
|
|
||||||
# Установить параметры
|
|
||||||
./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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
После изменения конфига скрипт автоматически перезапускает Hermes.
|
## `hermes` / `opencode` — сам агент
|
||||||
|
|
||||||
### Установка/обновление бинарников
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/harness install opencode # curl-установщик opencode.ai
|
./scripts/harness hermes config # показать конфиг
|
||||||
./scripts/harness install hermes # hermes update + рестарт сервисов
|
./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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Перезапуск
|
`hermes config --set` меняет `~/.hermes/config.yaml` и перезапускает сервисы.
|
||||||
|
|
||||||
```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/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Как добавить скилл в каталог
|
## Как добавить скилл в каталог
|
||||||
|
|
||||||
@@ -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"
|
git commit -m "chore: add user-skills to catalog"
|
||||||
```
|
```
|
||||||
|
|
||||||
После этого `harness list catalog` покажет все найденные скиллы, а
|
После этого `harness skills list` покажет найденные скиллы, а
|
||||||
`harness update catalog` подтянет свежие коммиты во всех submodule'ах.
|
`harness catalog update` подтянет свежие коммиты во всех submodule'ах.
|
||||||
|
|
||||||
Обычный клон (`git clone` внутрь `catalog/`) тоже работает — `update catalog`
|
|
||||||
обновит его через `git pull`, но такой каталог не попадёт в коммит репы
|
|
||||||
(вложенная репа не станет submodule автоматически).
|
|
||||||
|
|
||||||
### Раскладка репозитория и ручная установка
|
### Раскладка репозитория и ручная установка
|
||||||
|
|
||||||
CLI `harness` понимает одну раскладку — `<репа>/skills/<name>/SKILL.md`.
|
CLI понимает одну раскладку — `<репа>/skills/<name>/SKILL.md`. Репозитории с другой
|
||||||
Репозитории с другой структурой он показывает как `(no skills/)`, и их скиллы
|
структурой (`SKILL.md` в корне, `.agents/skills/`, вложенные категории) он показывает
|
||||||
ставятся вручную. Ограничение только у CLI: сам OpenCode сканирует рекурсивно
|
без списка скиллов, и такие скиллы ставятся вручную. Ограничение только у CLI: сам
|
||||||
(`skills/**/SKILL.md`, а через `skills.paths` — `**/SKILL.md` на любой глубине).
|
OpenCode сканирует рекурсивно (`skills/**/SKILL.md`, а через `skills.paths` —
|
||||||
|
`**/SKILL.md` на любой глубине).
|
||||||
|
|
||||||
Пример — `developer-documentation-skill` держит `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:
|
Формат — как у OpenCode:
|
||||||
|
|
||||||
@@ -215,9 +242,7 @@ permission:
|
|||||||
## Поддерживаемые форматы
|
## Поддерживаемые форматы
|
||||||
|
|
||||||
- **Скиллы** — `SKILL.md` с YAML frontmatter (`name`, `description`); родной формат OpenCode и Hermes.
|
- **Скиллы** — `SKILL.md` с YAML frontmatter (`name`, `description`); родной формат OpenCode и Hermes.
|
||||||
Скиллы из `catalog/*/skills/<name>/SKILL.md` видны через `harness list catalog`.
|
|
||||||
- **Агенты** — `<name>.md` с frontmatter (`description`, `mode`, ...); формат OpenCode.
|
- **Агенты** — `<name>.md` с frontmatter (`description`, `mode`, ...); формат OpenCode.
|
||||||
Лежат в `agents/<name>.md`.
|
|
||||||
|
|
||||||
## Совместимость
|
## Совместимость
|
||||||
|
|
||||||
|
|||||||
+347
-218
@@ -1,26 +1,28 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Harness — универсальный менеджер скиллов, агентов, MCP-серверов и конфигов
|
Harness — менеджер скиллов, агентов, MCP-серверов и конфигов
|
||||||
для Hermes и OpenCode.
|
для 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 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
|
import argparse
|
||||||
@@ -130,39 +132,27 @@ def read_frontmatter_field(path, field, width=None):
|
|||||||
# ─── Catalog ──────────────────────────────────────────────────────
|
# ─── Catalog ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def list_catalog():
|
def catalog_entries():
|
||||||
"""Scan catalog/ for skill sources (git repos with skills/ dirs)."""
|
"""Источники каталога: [(name, dir, [skill_names])], отсортировано."""
|
||||||
|
entries = []
|
||||||
if not CATALOG_DIR.exists():
|
if not CATALOG_DIR.exists():
|
||||||
print(" (empty — no catalog entries)")
|
return 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
|
|
||||||
for entry in sorted(CATALOG_DIR.iterdir()):
|
for entry in sorted(CATALOG_DIR.iterdir()):
|
||||||
if not entry.is_dir():
|
if not entry.is_dir():
|
||||||
continue
|
continue
|
||||||
skills_dir = entry / "skills"
|
skills_dir = entry / "skills"
|
||||||
if not skills_dir.is_dir():
|
names = []
|
||||||
continue
|
if skills_dir.is_dir():
|
||||||
for skill_dir in skills_dir.iterdir():
|
names = sorted(s.name for s in skills_dir.iterdir() if (s / "SKILL.md").exists())
|
||||||
if (skill_dir / "SKILL.md").exists():
|
entries.append((entry.name, entry, names))
|
||||||
yield entry.name, skill_dir.name, skill_dir
|
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():
|
def _submodule_paths():
|
||||||
@@ -180,6 +170,54 @@ def _submodule_paths():
|
|||||||
return 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():
|
def update_catalog():
|
||||||
"""Обновить каталог: submodule'ы — через суперпроект, клоны — git pull."""
|
"""Обновить каталог: submodule'ы — через суперпроект, клоны — git pull."""
|
||||||
if not CATALOG_DIR.exists():
|
if not CATALOG_DIR.exists():
|
||||||
@@ -239,8 +277,8 @@ def install_skill(skill_source_name, skill_name, target):
|
|||||||
skill_dir = candidate
|
skill_dir = candidate
|
||||||
|
|
||||||
if not skill_dir:
|
if not skill_dir:
|
||||||
die(f"Skill '{skill_source_name}/{skill_name}' not found in catalog. "
|
die(f"Скилл '{skill_source_name}/{skill_name}' не найден в каталоге. "
|
||||||
f"Use 'harness list catalog' to see available skills.")
|
f"Что есть — 'harness skills list'.")
|
||||||
|
|
||||||
targets = resolve_targets(target)
|
targets = resolve_targets(target)
|
||||||
|
|
||||||
@@ -259,16 +297,16 @@ def install_all_skills(source_name, target):
|
|||||||
source_dir = CATALOG_DIR / source_name
|
source_dir = CATALOG_DIR / source_name
|
||||||
skills_dir = source_dir / "skills"
|
skills_dir = source_dir / "skills"
|
||||||
if not skills_dir.is_dir():
|
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())
|
skills = sorted(s.name for s in skills_dir.iterdir() if (s / "SKILL.md").exists())
|
||||||
if not skills:
|
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:
|
for skill_name in skills:
|
||||||
install_skill(source_name, skill_name, target)
|
install_skill(source_name, skill_name, target)
|
||||||
ok(f"All {len(skills)} skills installed")
|
ok(f"Готово: {len(skills)} скиллов")
|
||||||
|
|
||||||
|
|
||||||
def _remove_path(path):
|
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)
|
dest_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if dest_dir.exists():
|
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)
|
_remove_path(dest_dir)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -293,35 +331,126 @@ def _install_skill_dir(src_dir, dest_dir, agent_name):
|
|||||||
except OSError:
|
except OSError:
|
||||||
# Fallback to copy
|
# Fallback to copy
|
||||||
shutil.copytree(src_dir, dest_dir, symlinks=True)
|
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):
|
def remove_skill(skill_name, target):
|
||||||
"""Remove a skill from target agent."""
|
"""Remove a skill from target agent."""
|
||||||
targets = resolve_targets(target)
|
targets = resolve_targets(target)
|
||||||
|
removed = 0
|
||||||
for t in targets:
|
for t in targets:
|
||||||
if t == "hermes":
|
if t == "hermes":
|
||||||
paths = list(HERMES_SKILLS.glob(f"*/{skill_name}")) + list(HERMES_SKILLS.glob(f"**/{skill_name}"))
|
paths = list(HERMES_SKILLS.glob(f"*/{skill_name}")) + list(HERMES_SKILLS.glob(f"**/{skill_name}"))
|
||||||
for p in paths:
|
for p in paths:
|
||||||
if p.exists():
|
if p.exists():
|
||||||
_remove_path(p)
|
_remove_path(p)
|
||||||
ok(f" [hermes] removed {p}")
|
ok(f" [hermes] удалён {p}")
|
||||||
|
removed += 1
|
||||||
elif t == "opencode":
|
elif t == "opencode":
|
||||||
for skill_root in OPENCODE_SKILLS_DIRS:
|
for skill_root in OPENCODE_SKILLS_DIRS:
|
||||||
dest = skill_root / skill_name
|
dest = skill_root / skill_name
|
||||||
if dest.exists():
|
if dest.exists():
|
||||||
_remove_path(dest)
|
_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):
|
def _skill_dirs(roots):
|
||||||
"""List installed skills."""
|
"""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)
|
targets = resolve_targets(target)
|
||||||
for t in targets:
|
for t in targets:
|
||||||
if t == "hermes":
|
if t == "hermes":
|
||||||
print(f"\n{c('Hermes skills', 'bold')} ({HERMES_SKILLS}):")
|
print(f"\n{c('Hermes skills', 'bold')} ({HERMES_SKILLS}):")
|
||||||
_print_skill_tree(HERMES_SKILLS)
|
_print_skill_tree(HERMES_SKILLS)
|
||||||
elif t == "opencode":
|
else:
|
||||||
for skill_root in OPENCODE_SKILLS_DIRS:
|
for skill_root in OPENCODE_SKILLS_DIRS:
|
||||||
if skill_root.exists():
|
if skill_root.exists():
|
||||||
print(f"\n{c('OpenCode skills', 'bold')} ({skill_root}):")
|
print(f"\n{c('OpenCode skills', 'bold')} ({skill_root}):")
|
||||||
@@ -586,7 +715,7 @@ def _parse_value(value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
# ─── Install Agents Binaries ──────────────────────────────────────
|
# ─── Binaries ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def install_opencode():
|
def install_opencode():
|
||||||
@@ -602,12 +731,11 @@ def install_opencode():
|
|||||||
die(f"OpenCode install failed: {err}")
|
die(f"OpenCode install failed: {err}")
|
||||||
|
|
||||||
|
|
||||||
def install_hermes():
|
def update_hermes():
|
||||||
info("Updating Hermes...")
|
info("Updating Hermes...")
|
||||||
rc, out, err = run(["hermes", "update"], timeout=120)
|
rc, out, err = run(["hermes", "update"], timeout=120)
|
||||||
if rc == 0:
|
if rc == 0:
|
||||||
ok("Hermes updated")
|
ok("Hermes updated")
|
||||||
# Restart services
|
|
||||||
restart_hermes()
|
restart_hermes()
|
||||||
else:
|
else:
|
||||||
warn(f"hermes update failed: {err}")
|
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))
|
enabled = sum(1 for s in servers.values() if s.get("enabled", True))
|
||||||
print(f" MCP servers: {c(str(len(servers)), 'cyan')} ({enabled} enabled)")
|
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(count_skills('hermes')), 'cyan')}")
|
||||||
print(f" Skills: {c(str(skill_count), 'cyan')}")
|
|
||||||
|
|
||||||
# OpenCode
|
# OpenCode
|
||||||
print(f"\n{c('OpenCode', 'bold')}")
|
print(f"\n{c('OpenCode', 'bold')}")
|
||||||
@@ -692,11 +819,7 @@ def show_status():
|
|||||||
for svc in OPENCODE_SERVICES:
|
for svc in OPENCODE_SERVICES:
|
||||||
_service_line(svc)
|
_service_line(svc)
|
||||||
|
|
||||||
oc_skills = 0
|
print(f" Skills: {c(str(count_skills('opencode')), 'cyan')}")
|
||||||
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')}")
|
|
||||||
|
|
||||||
oc_agents = len(list(OPENCODE_AGENTS_GLOBAL.glob("*.md"))) if OPENCODE_AGENTS_GLOBAL.exists() else 0
|
oc_agents = len(list(OPENCODE_AGENTS_GLOBAL.glob("*.md"))) if OPENCODE_AGENTS_GLOBAL.exists() else 0
|
||||||
repo_agents = len(available_agents())
|
repo_agents = len(available_agents())
|
||||||
@@ -704,13 +827,13 @@ def show_status():
|
|||||||
|
|
||||||
# Catalog
|
# Catalog
|
||||||
print(f"\n{c('Catalog', 'bold')}")
|
print(f"\n{c('Catalog', 'bold')}")
|
||||||
if CATALOG_DIR.exists():
|
entries = catalog_entries()
|
||||||
entries = [d for d in CATALOG_DIR.iterdir() if d.is_dir()]
|
if entries:
|
||||||
print(f" Sources: {c(str(len(entries)), 'cyan')}")
|
print(f" Sources: {c(str(len(entries)), 'cyan')}")
|
||||||
for e in entries:
|
for name, entry, skills in entries:
|
||||||
skills_dir = e / "skills"
|
sha, drift = source_git_state(entry)
|
||||||
n = len(list(skills_dir.glob("*/SKILL.md"))) if skills_dir.exists() else 0
|
mark = c(" ⚠ сдвинут", "yellow") if drift else ""
|
||||||
print(f" ├─ {c(e.name, 'cyan')} ({n} skills)")
|
print(f" ├─ {c(name, 'cyan')} ({len(skills)} skills){mark}")
|
||||||
else:
|
else:
|
||||||
print(f" {c('Not found', 'red')}")
|
print(f" {c('Not found', 'red')}")
|
||||||
|
|
||||||
@@ -722,190 +845,196 @@ def show_status():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Harness — Manager for Hermes & OpenCode skills, agents, MCP, and configs",
|
description="Harness — менеджер скиллов, агентов, MCP и конфигов для Hermes и OpenCode",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog="""
|
epilog="""
|
||||||
Examples:
|
Грамматика: объект — первым, действие — вторым.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
harness status
|
harness status
|
||||||
harness list catalog
|
|
||||||
harness list agents
|
harness skills list
|
||||||
harness install skill jakubkrehel-skills better-ui --target all
|
harness skills installed
|
||||||
harness install skill jakubkrehel-skills --all --target opencode
|
harness skills install jakubkrehel-skills better-ui --target all
|
||||||
harness install agent leader --scope global
|
harness skills install jakubkrehel-skills --all --target opencode
|
||||||
harness install agent --all --scope project --project ~/work/myrepo
|
harness skills remove better-ui --target hermes
|
||||||
harness install opencode
|
|
||||||
harness configure hermes --set model.default=gpt-4o
|
harness agents list
|
||||||
harness configure hermes mcp add my-server http://localhost:8000/mcp
|
harness agents install leader --scope global
|
||||||
harness configure hermes mcp add github npx --env GITHUB_TOKEN=xxx --arg -y --arg @modelcontextprotocol/server-github
|
harness agents install --all --scope project --project ~/work/myrepo
|
||||||
harness remove skill better-ui --target hermes
|
harness agents remove leader
|
||||||
harness remove agent leader
|
|
||||||
harness update catalog
|
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")
|
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
|
# ── status
|
||||||
install_parser = sub.add_parser("install", help="Install things")
|
sub.add_parser("status", help="Обзор среды (сервисы, версии, счётчики)")
|
||||||
install_sub = install_parser.add_subparsers(dest="install_type", required=True)
|
|
||||||
|
|
||||||
# install skill
|
# ── skills
|
||||||
skill_parser = install_sub.add_parser("skill")
|
skills_p = sub.add_parser("skills", help="Скиллы из каталога")
|
||||||
skill_parser.add_argument("source", help="Catalog source name (e.g. jakubkrehel-skills)")
|
skills_sub = skills_p.add_subparsers(dest="action", required=True, metavar="<действие>")
|
||||||
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")
|
|
||||||
|
|
||||||
# install agent (OpenCode only)
|
sk_list = skills_sub.add_parser("list", help="Что доступно в каталоге (+ где уже стоит)")
|
||||||
agent_parser = install_sub.add_parser("agent", help="Install OpenCode agent definition(s)")
|
sk_list.add_argument("--target", default="all", choices=["hermes", "opencode", "all"])
|
||||||
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)")
|
|
||||||
|
|
||||||
# install opencode / hermes
|
sk_inst = skills_sub.add_parser("installed", help="Что реально установлено у агентов")
|
||||||
install_sub.add_parser("opencode", help="Install/update OpenCode binary")
|
sk_inst.add_argument("--target", default="all", choices=["hermes", "opencode", "all"])
|
||||||
install_sub.add_parser("hermes", help="Update Hermes")
|
|
||||||
|
|
||||||
# list
|
sk_add = skills_sub.add_parser("install", help="Установить скилл(ы) из каталога")
|
||||||
list_parser = sub.add_parser("list", help="List installed items")
|
sk_add.add_argument("source", help="Источник в каталоге (напр. jakubkrehel-skills)")
|
||||||
list_sub = list_parser.add_subparsers(dest="list_type", required=True)
|
sk_add.add_argument("skill", nargs="?", default=None, help="Имя скилла (или --all)")
|
||||||
list_sub.add_parser("skills", help="List installed skills").add_argument(
|
sk_add.add_argument("--all", action="store_true", help="Все скиллы источника")
|
||||||
"--target", default="all", choices=["hermes", "opencode", "all"]
|
sk_add.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")
|
|
||||||
|
|
||||||
# remove
|
sk_rm = skills_sub.add_parser("remove", help="Удалить скилл у агентов")
|
||||||
remove_parser = sub.add_parser("remove", help="Remove items")
|
sk_rm.add_argument("name", help="Имя скилла")
|
||||||
remove_sub = remove_parser.add_subparsers(dest="remove_type", required=True)
|
sk_rm.add_argument("--target", default="all", choices=["hermes", "opencode", "all"])
|
||||||
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")
|
|
||||||
|
|
||||||
# configure
|
# ── agents
|
||||||
configure_parser = sub.add_parser("configure", help="Configure agents")
|
agents_p = sub.add_parser("agents", help="Агенты (только OpenCode)")
|
||||||
configure_sub = configure_parser.add_subparsers(dest="configure_type", required=True)
|
agents_sub = agents_p.add_subparsers(dest="action", required=True, metavar="<действие>")
|
||||||
hermes_conf = configure_sub.add_parser("hermes", help="Configure Hermes")
|
|
||||||
hermes_conf_sub = hermes_conf.add_subparsers(dest="hermes_action")
|
|
||||||
|
|
||||||
# configure hermes (plain — show or set)
|
ag_list = agents_sub.add_parser("list", help="Доступные в репо и установленные агенты")
|
||||||
hermes_conf.add_argument("--set", "-s", action="append", default=[],
|
ag_list.add_argument("--scope", default="all", choices=["global", "project", "all"])
|
||||||
help="key=value pairs (repeatable). Omit to show config.")
|
ag_list.add_argument("--project", default=None, help="Корень проекта (по умолчанию cwd)")
|
||||||
|
|
||||||
# configure hermes mcp
|
ag_add = agents_sub.add_parser("install", help="Установить агента(ов)")
|
||||||
mcp_parser = hermes_conf_sub.add_parser("mcp", help="Manage Hermes MCP servers")
|
ag_add.add_argument("name", nargs="?", default=None, help="Имя агента (или --all)")
|
||||||
mcp_sub = mcp_parser.add_subparsers(dest="mcp_action", required=True)
|
ag_add.add_argument("--all", action="store_true", help="Все агенты")
|
||||||
mcp_add = mcp_sub.add_parser("add")
|
ag_add.add_argument("--scope", default="global", choices=["global", "project", "all"],
|
||||||
mcp_add.add_argument("name")
|
help="global → ~/.config/opencode/agents; project → <project>/.opencode/agents")
|
||||||
mcp_add.add_argument("source", help="URL or command")
|
ag_add.add_argument("--project", default=None, help="Корень проекта (по умолчанию cwd)")
|
||||||
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)")
|
|
||||||
|
|
||||||
mcp_rm = mcp_sub.add_parser("rm")
|
ag_rm = agents_sub.add_parser("remove", help="Удалить агента")
|
||||||
mcp_rm.add_argument("name", help="MCP server name to remove")
|
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
|
# ── mcp
|
||||||
restart_parser = sub.add_parser("restart", help="Restart agents")
|
mcp_p = sub.add_parser("mcp", help="MCP-серверы Hermes")
|
||||||
restart_parser.add_argument("target", choices=["hermes", "opencode"], help="Agent to restart")
|
mcp_sub = mcp_p.add_subparsers(dest="action", required=True, metavar="<действие>")
|
||||||
|
|
||||||
# status
|
mcp_sub.add_parser("list", help="Список MCP-серверов")
|
||||||
sub.add_parser("status", help="Show status of everything")
|
|
||||||
|
|
||||||
# update
|
mcp_add_p = mcp_sub.add_parser("add", help="Добавить MCP-сервер (+ рестарт gateway)")
|
||||||
sub.add_parser("update", help="Update catalog").add_argument("type", nargs="?", default="catalog")
|
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()
|
args = parser.parse_args()
|
||||||
os.makedirs(CATALOG_DIR, exist_ok=True)
|
os.makedirs(CATALOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
# ── Commands ──────────────────────────────────────────────
|
# ── Dispatch ──────────────────────────────────────────────
|
||||||
|
|
||||||
if args.command == "install":
|
obj = args.object
|
||||||
if args.install_type == "skill":
|
action = getattr(args, "action", None)
|
||||||
source = args.source
|
|
||||||
skill = args.skill
|
|
||||||
|
|
||||||
|
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:
|
if "/" in source and not skill:
|
||||||
source, skill = source.split("/", 1)
|
source, skill = source.split("/", 1)
|
||||||
|
|
||||||
if args.all:
|
if args.all:
|
||||||
install_all_skills(source, args.target)
|
install_all_skills(source, args.target)
|
||||||
elif skill is None:
|
elif skill is None:
|
||||||
die("Specify skill name: harness install skill <source> <skill> or use --all")
|
die("Укажи имя скилла или --all: harness skills install <источник> <скилл>")
|
||||||
else:
|
else:
|
||||||
install_skill(source, skill, args.target)
|
install_skill(source, skill, args.target)
|
||||||
|
elif action == "remove":
|
||||||
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":
|
|
||||||
remove_skill(args.name, args.target)
|
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)
|
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)
|
mcp_rm(args.name)
|
||||||
|
|
||||||
elif args.command == "configure":
|
elif obj == "catalog":
|
||||||
if args.configure_type == "hermes":
|
if action == "list":
|
||||||
if hasattr(args, "hermes_action") and args.hermes_action == "mcp":
|
list_catalog()
|
||||||
if args.mcp_action == "add":
|
elif action == "update":
|
||||||
# Combine --args and --arg
|
update_catalog()
|
||||||
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 args.command == "restart":
|
elif obj == "hermes":
|
||||||
if args.target == "hermes":
|
if action == "config":
|
||||||
|
configure_hermes(args.set)
|
||||||
|
elif action == "restart":
|
||||||
restart_hermes()
|
restart_hermes()
|
||||||
else:
|
elif action == "update":
|
||||||
|
update_hermes()
|
||||||
|
|
||||||
|
elif obj == "opencode":
|
||||||
|
if action == "install":
|
||||||
|
install_opencode()
|
||||||
|
elif action == "restart":
|
||||||
restart_opencode()
|
restart_opencode()
|
||||||
|
|
||||||
elif args.command == "status":
|
|
||||||
show_status()
|
|
||||||
|
|
||||||
elif args.command == "update":
|
|
||||||
update_catalog()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user