#!/usr/bin/env bash # ============================================================================= # adapters/opencode/adapter.sh — Opencode framework adapter # ============================================================================= # Sourced by scripts/build.sh AFTER adapters/lib.sh. # Translates source files into a dist/opencode/ tree that mirrors what # opencode expects in the user's vault. # ============================================================================= # shellcheck source=adapters/opencode/config-merge.sh source "$(dirname "${BASH_SOURCE[0]}")/config-merge.sh" OC_PLATFORM="opencode" OC_FW_DIR="opencode" OC_DISPATCHER="AGENTS.md" # Capability → opencode permission key. Returns the permission key to set to # "allow" for each capability, or empty string for capabilities that have no # opencode equivalent (they are dropped). # # Reference (spec §"Capability vocabulary"): # read → implicit, no permission needed # write → edit: allow # edit → edit: allow # bash → bash: allow # webfetch → webfetch: allow # websearch → drop (no equivalent) # notebook → drop # task → drop (subagent invocation, not a permission) # todo → drop oc_capability_to_permission() { local cap="$1" case "$cap" in read) echo "" ;; # implicit write) echo "edit" ;; edit) echo "edit" ;; bash) echo "bash" ;; webfetch) echo "webfetch" ;; websearch) echo "" ;; # drop notebook) echo "" ;; # drop task) echo "" ;; # drop todo) echo "" ;; # drop *) echo "" ;; esac } # Event vocabulary → opencode native event name. oc_event_to_native() { local event="$1" case "$event" in before-tool-use) echo "tool.execute.before" ;; after-tool-use) echo "tool.execute.after" ;; on-notification) echo "session.idle" ;; on-session-start) echo "session.created" ;; on-prompt-submit) echo "tui.prompt.append" ;; *) echo "" ;; esac } # Neutral model tier → opencode provider/model id. Conservative mapping: # if the source model is already provider-prefixed (contains "/"), pass through # unchanged. Otherwise look up in the table and fall back to the raw value. oc_model_to_provider() { local model="$1" case "$model" in */*) echo "$model" ;; # already qualified low) echo "anthropic/claude-haiku-4-5" ;; mid) echo "anthropic/claude-sonnet-4-5" ;; high) echo "anthropic/claude-opus-4-5" ;; *) echo "$model" ;; # unknown — passthrough esac } # adapter_translate_dispatcher # Copies the source DISPATCHER.md to dest_dir/AGENTS.md (opencode's vault-root # dispatcher filename). Rewrites .platform/ and DISPATCHER.md to opencode paths. adapter_translate_dispatcher() { local src="$1" dst="$2" [[ -f "$src" ]] || return 0 mkdir -p "$dst" cp "$src" "$dst/AGENTS.md" rewrite_platform_paths "$dst/AGENTS.md" "$OC_FW_DIR" "$OC_DISPATCHER" } # adapter_translate_references # Copies *.md into dest_root/.opencode/references/, rewriting framework paths. adapter_translate_references() { local src="$1" dst="$2" [[ -d "$src" ]] || return 0 local out="$dst/.opencode/references" mkdir -p "$out" for f in "$src"/*.md; do [[ -f "$f" ]] || continue should_include "$f" "$OC_PLATFORM" || continue cp "$f" "$out/" rewrite_platform_paths "$out/$(basename "$f")" "$OC_FW_DIR" "$OC_DISPATCHER" done } # adapter_translate_skills # Copies each skill directory's SKILL.md into dest_root/.opencode/skills//, # rewriting framework paths in the body. adapter_translate_skills() { local src="$1" dst="$2" [[ -d "$src" ]] || return 0 for skill_dir in "$src"/*/; do [[ -f "${skill_dir}SKILL.md" ]] || continue should_include "${skill_dir}SKILL.md" "$OC_PLATFORM" || continue local name; name="$(basename "$skill_dir")" local out="$dst/.opencode/skills/$name" mkdir -p "$out" cp "${skill_dir}SKILL.md" "$out/SKILL.md" rewrite_platform_paths "$out/SKILL.md" "$OC_FW_DIR" "$OC_DISPATCHER" done } # adapter_translate_agents # For each *.md in source_agents_dir, translate the capabilities frontmatter # into an opencode permission block, map the model, and write to # dest_root/.opencode/agents/.md. adapter_translate_agents() { local src="$1" dst="$2" [[ -d "$src" ]] || return 0 local out_dir="$dst/.opencode/agents" mkdir -p "$out_dir" while IFS= read -r agent; do [[ -f "$agent" ]] || continue should_include "$agent" "$OC_PLATFORM" || continue local model_raw; model_raw="$(parse_frontmatter "$agent" model)" local mode_raw; mode_raw="$(parse_frontmatter "$agent" mode)" local caps; caps="$(parse_capabilities "$agent")" local model_out; model_out="$(oc_model_to_provider "$model_raw")" local mode_out="${mode_raw:-subagent}" # Build a unique permission list local perms="" for cap in $caps; do local p; p="$(oc_capability_to_permission "$cap")" [[ -z "$p" ]] && continue # Dedupe: skip if already in $perms (space-delimited) case " $perms " in *" $p "*) ;; *) perms="$perms $p" ;; esac done perms="${perms# }" local out_file="$out_dir/$(basename "$agent")" { echo "---" # Copy description block verbatim (may be folded YAML with continuation lines) awk '/^---$/{n++; next} n==1 && /^description:/{print; in_desc=1; next} n==1 && in_desc && /^[[:space:]]/{print; next} n==1 && in_desc && !/^[[:space:]]/{in_desc=0} n>=2{exit}' "$agent" echo "mode: $mode_out" echo "model: $model_out" if [[ -z "$perms" ]]; then echo "permission: {}" else echo "permission:" for p in $perms; do echo " $p: allow" done fi echo "---" echo "" agent_body "$agent" } > "$out_file" rewrite_platform_paths "$out_file" "$OC_FW_DIR" "$OC_DISPATCHER" done < <(enumerate_agents "$src") } # _oc_hook_registry_json # Emits a JSON array literal representing the hook registry, suitable for # substituting into the plugin template. Each entry: {name, script, triggers: [{event, matchTool}]}. # Script paths are stored as "../hooks/" so the plugin (at .opencode/plugins/) # can reach .opencode/hooks/ at runtime via __dirname + path join. _oc_hook_registry_json() { local src="$1" local entries='[]' while IFS= read -r yaml; do [[ -f "$yaml" ]] || continue should_include "$yaml" "$OC_PLATFORM" || continue local meta; meta="$(parse_hook_yaml "$yaml")" local name; name="$(echo "$meta" | grep '^name=' | head -1 | cut -d= -f2-)" local script; script="$(echo "$meta" | grep '^script=' | head -1 | cut -d= -f2-)" local event; event="$(echo "$meta" | grep '^event=' | head -1 | cut -d= -f2-)" local match_tool; match_tool="$(echo "$meta" | grep '^match-tool=' | head -1 | cut -d= -f2- || true)" local oc_event; oc_event="$(oc_event_to_native "$event")" # Build the matchTool JSON array (empty array if no filter) local match_json='[]' if [[ -n "$match_tool" ]]; then match_json="$(echo "$match_tool" | jq -R 'split(" ") | map(select(length > 0))')" fi # Use ../hooks/