793a14e338
Includes agents, skills, scripts, container launch scripts, and project configuration files (opencode.json, crush.json, Dockerfile).
220 lines
6.5 KiB
Python
Executable File
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()
|