From 3f5d89bee3cbbbe2c38f99075e7ab5ab3ae30ba2 Mon Sep 17 00:00:00 2001 From: nunziati Date: Wed, 8 Apr 2026 21:51:37 +0000 Subject: [PATCH] Implement opencode adapter. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: win0na feat(lib.sh): add install_plugins helper for opencode JS plugins build(adapters): opencode adapter skeleton with capability/event tables build(opencode): adapter_translate_dispatcher (DISPATCHER.md → AGENTS.md) build(opencode): adapter_translate_references and adapter_translate_skills Implements Task 4 and Task 5: - adapter_translate_references: Copies reference markdown files to .opencode/references/ - adapter_translate_skills: Copies skill SKILL.md files to .opencode/skills// with exclude filtering Both functions respect framework filtering via should_include(). Co-Authored-By: Claude Sonnet 4.6 build(opencode): adapter_translate_agents with capability→permission mapping Co-Authored-By: Claude Sonnet 4.6 build(opencode): bash-executor template for spawning hook scripts build(opencode): plugin-stub template for mbifc-hooks.js build(opencode): adapter_translate_hooks with JS plugin generation Implements _oc_hook_registry_json and adapter_translate_hooks in the opencode adapter. Copies hook scripts to .opencode/hooks/, generates a single .opencode/plugins/mbifc-hooks.js by inlining bash-executor.js and synthesising a hook registry from *.hook.yaml files. Uses python3 for template substitution to safely handle multi-line JS content. Adds 3 unit tests (copies scripts, registry entries, noop when no hooks dir). Co-Authored-By: Claude Sonnet 4.6 build(opencode): adapter_translate_mcp with local/remote handling build(opencode): adapter_finalize and complete adapter_build wiring Add adapter_finalize placeholder and wire adapter_translate_mcp into adapter_build; add end-to-end integration test (14/14 pass). Co-Authored-By: Claude Sonnet 4.6 feat(launchme): branch on --framework for opencode install layout Co-Authored-By: Claude Sonnet 4.6 feat(updateme): branch on --framework for opencode install layout Mirror the same case "$FRAMEWORK" block from launchme.sh: framework-specific DIST_COMPONENTS_DIR, VAULT_COMPONENTS_DIR, DISPATCHER_SRC/DST, MCP_SRC/DST, HAS_PLUGINS; conditional install_plugins; conditional install_settings; framework-aware vault-setup check; framework-neutral summary messages. Co-Authored-By: Claude Sonnet 4.6 --- adapters/opencode/adapter.sh | 370 ++++++++++++++++++ adapters/opencode/templates/bash-executor.js | 43 ++ .../opencode/templates/plugin-stub.js.tmpl | 88 +++++ scripts/launchme.sh | 56 ++- scripts/lib.sh | 24 ++ scripts/updateme.sh | 78 +++- tests/adapters/opencode/adapter.test.sh | 341 ++++++++++++++++ 7 files changed, 972 insertions(+), 28 deletions(-) create mode 100755 adapters/opencode/adapter.sh create mode 100755 adapters/opencode/templates/bash-executor.js create mode 100755 adapters/opencode/templates/plugin-stub.js.tmpl create mode 100755 tests/adapters/opencode/adapter.test.sh diff --git a/adapters/opencode/adapter.sh b/adapters/opencode/adapter.sh new file mode 100755 index 0000000..1f13a46 --- /dev/null +++ b/adapters/opencode/adapter.sh @@ -0,0 +1,370 @@ +#!/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. +# ============================================================================= + +FRAMEWORK="opencode" + +# 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 +} + +# CC-style short model name → 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 + sonnet) echo "anthropic/claude-sonnet-4-5" ;; + opus) echo "anthropic/claude-opus-4-5" ;; + haiku) echo "anthropic/claude-haiku-4-5" ;; + *) echo "$model" ;; # unknown — pass through + esac +} + +# adapter_translate_dispatcher +# Copies the source DISPATCHER.md to dest_dir/AGENTS.md (opencode's vault-root +# dispatcher filename). No content translation. +adapter_translate_dispatcher() { + local src="$1" dst="$2" + [[ -f "$src" ]] || return 0 + mkdir -p "$dst" + cp "$src" "$dst/AGENTS.md" +} + +# adapter_translate_references +# Verbatim copy of *.md into dest_root/.opencode/references/. +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" "$FRAMEWORK" || continue + cp "$f" "$out/" + done +} + +# adapter_translate_skills +# Copies each skill directory's SKILL.md into dest_root/.opencode/skills//. +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" "$FRAMEWORK" || continue + local name; name="$(basename "$skill_dir")" + local out="$dst/.opencode/skills/$name" + mkdir -p "$out" + cp "${skill_dir}SKILL.md" "$out/SKILL.md" + done +} + +# _oc_flatten_description +# Reads the description field from the source agent, collapsing any YAML +# folded continuation lines into a single line with whitespace normalised. +# Echoes the bare description text with no leading/trailing quotes. +_oc_flatten_description() { + local file="$1" + awk ' + /^---$/ { fm++; next } + fm == 1 && /^description:/ { + sub(/^description:[[:space:]]*/, "") + desc = $0 + in_desc = 1 + next + } + fm == 1 && in_desc && /^[[:space:]]/ { + sub(/^[[:space:]]+/, "") + desc = desc " " $0 + next + } + fm == 1 && in_desc { in_desc = 0 } + fm >= 2 { exit } + END { + # Collapse runs of whitespace + gsub(/[[:space:]]+/, " ", desc) + sub(/^[[:space:]]+/, "", desc) + sub(/[[:space:]]+$/, "", desc) + print desc + } + ' "$file" +} + +# 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" "$FRAMEWORK" || 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 desc; desc="$(_oc_flatten_description "$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 "---" + echo "description: \"$desc\"" + 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" + 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" "$FRAMEWORK" || 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/