Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions install-symlinks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,41 @@ build_copy_opencode_commands() {
done
}

# Emit a slash-command shim for every Ring skill so it appears in opencode's
# TUI `/` autocomplete. The TUI silently filters source="skill" entries (see
# packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx), so
# without these shims only real command-source files show up. A real command
# at the same destination always wins — shims never overwrite.
build_make_opencode_skill_commands() {
local team="$1"
local src_dir="$RING_DIR/$team/skills"
local dst_dir="$OPENCODE_OUT/command/$team"
[ -d "$src_dir" ] || return 0
local skill_dir name src_md dst_md
for skill_dir in "$src_dir"/*/; do
[ -d "$skill_dir" ] || continue
name="$(basename "$skill_dir")"
[ "$name" = "shared-patterns" ] && continue
src_md="$skill_dir/SKILL.md"
[ -f "$src_md" ] || continue
dst_md="$dst_dir/$name.md"
if [ -e "$dst_md" ]; then
vlog "opencode skill-cmd: $team/$name (real command exists, skip shim)"
continue
fi
if [ "$DRY_RUN" -eq 1 ]; then
vlog "[dry-run] opencode skill-cmd shim: $team/$name -> $dst_md"
continue
fi
mkdir -p "$dst_dir"
python3 "$PY_HELPER" \
--emit-opencode-skill-shim \
--source "$src_md" \
--dest "$dst_md"
vlog "opencode skill-cmd: $team/$name"
done
}

build_codex_skill() {
local team="$1"; local skill_dir="$2"
local name dst_dir src_skill_md dst_skill_md
Expand Down Expand Up @@ -742,10 +777,11 @@ do_build() {
local team d
for team in $TEAMS; do
[ -d "$RING_DIR/$team" ] || { log_warn "team dir missing: $team"; continue; }
build_copy_opencode_agents "$team"
build_copy_opencode_skills "$team"
build_copy_opencode_commands "$team"
build_copy_shared_patterns_codex "$team"
build_copy_opencode_agents "$team"
build_copy_opencode_skills "$team"
build_copy_opencode_commands "$team"
build_make_opencode_skill_commands "$team"
build_copy_shared_patterns_codex "$team"

if [ -d "$RING_DIR/$team/skills" ]; then
for d in "$RING_DIR/$team/skills"/*/; do
Expand Down
57 changes: 57 additions & 0 deletions scripts/_codex_frontmatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
Or, to rewrite markdown link paths in an accessory (non-SKILL.md) file:
python3 scripts/_codex_frontmatter.py --rewrite-paths \
--source <path> --dest <path> --team <team> --lookup <lookup.json>

Or, to emit a slash-command shim for an opencode skill so it appears in the
TUI's `/` autocomplete (which silently filters source="skill" entries):
python3 scripts/_codex_frontmatter.py --emit-opencode-skill-shim \
--source <path-to-SKILL.md> --dest <path-to-shim.md>
"""

from __future__ import annotations
Expand Down Expand Up @@ -390,6 +395,44 @@ def rewrite_accessory(
os.replace(tmp, dest)


def emit_opencode_skill_shim(source: Path, dest: Path) -> None:
"""Emit an opencode slash-command shim for a Ring skill.

The opencode TUI silently filters commands whose source is "skill" out
of the `/` autocomplete (see packages/opencode/src/cli/cmd/tui/component/
prompt/autocomplete.tsx). Skills only appear when also registered as a
real command-source file. This writes a minimal shim that delegates to
the skill tool so the user can invoke it via `/ring:<skill-name>`.
"""
text = source.read_text(encoding="utf-8")
fm_lines, _ = _split_frontmatter(text, str(source))
groups = {key: lines for key, lines in _parse_top_level_keys(fm_lines)}

name_group = groups.get("name")
if not name_group:
raise ValueError("missing 'name' in frontmatter: " + str(source))
# Reuse description-collapsing logic to normalize 'name' (always inline in
# Ring skills, but the helper handles quoting/whitespace defensively).
name = _extract_description(name_group)

description_group = groups.get("description")
description = _extract_description(description_group) if description_group else ""

shim = (
"---\n"
f"name: {_yaml_inline_string(name)}\n"
f"description: {_yaml_inline_string(description)}\n"
"---\n"
"\n"
f"Invoke the `{name}` skill via the Skill tool. Arguments: $ARGUMENTS\n"
)

tmp = dest.with_suffix(dest.suffix + ".tmp")
tmp.parent.mkdir(parents=True, exist_ok=True)
tmp.write_text(shim, encoding="utf-8")
os.replace(tmp, dest)


def main(argv: list[str]) -> int:
p = argparse.ArgumentParser()
p.add_argument("--build-lookup", type=Path, default=None)
Expand All @@ -404,6 +447,11 @@ def main(argv: list[str]) -> int:
action="store_true",
help="Rewrite link paths in an accessory .md file only",
)
p.add_argument(
"--emit-opencode-skill-shim",
action="store_true",
help="Emit an opencode slash-command shim for a Ring skill",
)
args = p.parse_args(argv)

if args.build_lookup is not None:
Expand Down Expand Up @@ -432,6 +480,15 @@ def main(argv: list[str]) -> int:
rewrite_accessory(args.source, args.dest, args.team, lookup)
return 0

if args.emit_opencode_skill_shim:
required = (("--source", args.source), ("--dest", args.dest))
missing = [n for n, v in required if v is None]
if missing:
print("missing required args: " + ", ".join(missing), file=sys.stderr)
return 2
emit_opencode_skill_shim(args.source, args.dest)
return 0

missing = [
n
for n, v in (
Expand Down