Files
my-opencode-config/scripts/list-agents.py
T
hurui200320 0408c27e6e Initial commit: OpenCode configuration
Includes agents, skills, scripts, container launch scripts,
and project configuration files (opencode.json, crush.json, Dockerfile).

Add repo reference for skill container

Add workspace for persist container, used for testing cli

aa
2026-04-07 20:41:21 +08:00

220 lines
6.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Discover OpenCode agent files and list their name, mode, and description.
Scans two locations recursively for ``*.md`` agent definition files:
- **Project-level**: ``<cwd>/.opencode/agents/``
- **User-level**: ``~/.config/opencode/agents/``
Parses YAML frontmatter (delimited by ``---``) from each file using only
Python stdlib — no third-party dependencies required.
When agent names collide between project and user level, the project-level
definition takes precedence and the user-level duplicate is silently dropped.
"""
from __future__ import annotations
import argparse
import json
import os
import pathlib
# ---------------------------------------------------------------------------
# Frontmatter parser (stdlib only, handles block scalars)
# ---------------------------------------------------------------------------
def _parse_frontmatter(text: str) -> dict[str, str]:
"""Extract top-level scalar fields from YAML frontmatter.
Handles plain values, single-line quoted strings, and ``>-`` / ``>``
block scalars. Returns a dict mapping field names to their string
values. Nested structures are ignored — only the fields we care
about (``description``, ``mode``) are top-level scalars.
"""
lines = text.splitlines()
# Find the opening and closing ``---`` delimiters.
delimiters: list[int] = []
for idx, line in enumerate(lines):
if line.strip() == "---":
delimiters.append(idx)
if len(delimiters) == 2:
break
if len(delimiters) < 2:
return {}
front_lines = lines[delimiters[0] + 1 : delimiters[1]]
result: dict[str, str] = {}
current_key: str | None = None
current_value_parts: list[str] = []
block_scalar = False # True when inside a >- / > block scalar
def _flush() -> None:
if current_key is not None:
value = " ".join(current_value_parts).strip()
# Strip surrounding quotes if present.
if len(value) >= 2 and value[0] in ('"', "'") and value[-1] == value[0]:
value = value[1:-1]
result[current_key] = value
for line in front_lines:
stripped = line.strip()
# Detect a new top-level key (no leading whitespace).
if not line[:1].isspace() and ":" in stripped:
_flush()
key, _, raw_value = stripped.partition(":")
key = key.strip()
raw_value = raw_value.strip()
current_key = key
current_value_parts = []
block_scalar = False
if raw_value in (">-", ">", "|", "|-"):
block_scalar = True
elif raw_value:
current_value_parts.append(raw_value)
elif block_scalar and line[:1].isspace():
# Continuation line of a block scalar.
current_value_parts.append(stripped)
elif current_key is not None and line[:1].isspace() and not block_scalar:
# Indented continuation of a plain/quoted value.
current_value_parts.append(stripped)
_flush()
return result
# ---------------------------------------------------------------------------
# Agent discovery
# ---------------------------------------------------------------------------
def _discover_agents(
search_dir: pathlib.Path, source_label: str
) -> list[dict[str, str]]:
"""Recursively find ``*.md`` files under *search_dir* and parse them."""
agents: list[dict[str, str]] = []
if not search_dir.is_dir():
return agents
for md_path in sorted(search_dir.rglob("*.md")):
if not md_path.is_file():
continue
try:
text = md_path.read_text(encoding="utf-8")
except OSError:
continue
fields = _parse_frontmatter(text)
description = fields.get("description", "")
mode = fields.get("mode", "unknown")
agents.append(
{
"name": md_path.stem,
"mode": mode,
"source": source_label,
"description": description,
}
)
return agents
def discover_all(cwd: pathlib.Path) -> list[dict[str, str]]:
"""Return the merged agent list, project-level first.
When agent names collide, project-level wins.
"""
project_dir = cwd / ".opencode" / "agents"
user_dir = pathlib.Path.home() / ".config" / "opencode" / "agents"
project_agents = _discover_agents(project_dir, "project")
user_agents = _discover_agents(user_dir, "user")
seen_names: set[str] = {a["name"] for a in project_agents}
merged = list(project_agents)
for agent in user_agents:
if agent["name"] not in seen_names:
merged.append(agent)
seen_names.add(agent["name"])
return merged
# ---------------------------------------------------------------------------
# Output formatting
# ---------------------------------------------------------------------------
def _collapse(text: str) -> str:
"""Collapse multi-line text into a single line."""
return " ".join(text.split())
def format_lines(agents: list[dict[str, str]]) -> str:
"""Format agents as one pipe-delimited line per agent."""
if not agents:
return "No agents discovered."
lines: list[str] = [f"Discovered {len(agents)} agent(s):"]
for agent in agents:
desc = _collapse(agent["description"])
mode = agent["mode"]
lines.append(f"{agent['name']}|{mode}|{desc}")
return "\n".join(lines)
def format_json(agents: list[dict[str, str]]) -> str:
"""Format agents as a JSON array."""
return json.dumps(agents, indent=2)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Discover OpenCode agent files and list their name, mode, "
"and description.",
)
parser.add_argument(
"--cwd",
default=None,
help="Working directory to search for .opencode/agents/ "
"(default: current directory)",
)
parser.add_argument(
"--json",
dest="output_json",
action="store_true",
help="Output as JSON instead of pipe-delimited lines",
)
args = parser.parse_args()
cwd = pathlib.Path(args.cwd) if args.cwd else pathlib.Path(os.getcwd())
agents = discover_all(cwd)
if args.output_json:
print(format_json(agents))
else:
print(format_lines(agents))
if __name__ == "__main__":
main()