Files
my-opencode-config/scripts/list-agents.py
T
hurui200320 793a14e338 Initial commit: OpenCode configuration
Includes agents, skills, scripts, container launch scripts,
and project configuration files (opencode.json, crush.json, Dockerfile).
2026-04-07 08:29:07 +00: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()