- Add feynman as third target (~/.feynman/agent/agents) alongside opencode
and claude. Default targets now: opencode,claude,feynman. Each target
gets its own parked source dir (formats differ: opencode permission/mode,
claude PascalCase tools, feynman lowercase tools + thinking/output).
- Fix fzf {} preview shell-escape bug: {} in fzf is shell-escaped, so
"$PARKED/{}.md" embedded literal quotes into the path and broke preview.
Switched to "$PARKED"/{}.md across all 8 preview lines.
- pick: union-of-parked across all 3 targets for category list (so a
feynman-only agent like researcher is reachable). After agent multi-select
(rows annotated [oc,cl,fy] showing parked-in targets), prompt target
picker (all / opencode / claude / feynman, TAB for multi).
- disable-pick: rewritten to show union of actives across all 3 targets
with [oc,cl,fy] indicators, then target picker for which to disable from.
- cmd_status: 3-target counts (parked + active) with all-3 intersection.
- reindex: tracks parked.{opencode,claude,feynman} + active list across 3.
- New env vars: OPC_AGENTS_FEYNMAN_PARKED, OPC_AGENTS_FEYNMAN_ACTIVE.
862 lines
32 KiB
Bash
Executable File
862 lines
32 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# opc-agents — multi-target agent enable/disable manager (opencode + claude + feynman)
|
|
#
|
|
# Parked sources (per-target — formats differ):
|
|
# opencode → ~/Documents/personas/agents-opencode-archive (permission: { ... })
|
|
# claude → ~/Documents/personas/agents-claude-archive (tools: Read, Glob, ...)
|
|
# feynman → ~/Documents/personas/agents-feynman-archive (tools: lowercase, thinking, output)
|
|
#
|
|
# Active targets:
|
|
# opencode → ~/.config/opencode/agents
|
|
# claude → ~/.claude/agents
|
|
# feynman → ~/.feynman/agent/agents
|
|
#
|
|
# Default targets: opencode,claude,feynman (override via OPC_AGENTS_TARGETS or --target)
|
|
|
|
set -euo pipefail
|
|
|
|
# Backward compat: OPC_AGENTS_PARKED still works for opencode (legacy)
|
|
PARKED_OPENCODE="${OPC_AGENTS_PARKED:-$HOME/Documents/personas/agents-opencode-archive}"
|
|
PARKED_CLAUDE="${OPC_AGENTS_CLAUDE_PARKED:-$HOME/Documents/personas/agents-claude-archive}"
|
|
PARKED_FEYNMAN="${OPC_AGENTS_FEYNMAN_PARKED:-$HOME/Documents/personas/agents-feynman-archive}"
|
|
ACTIVE_OPENCODE="${OPC_AGENTS_ACTIVE:-$HOME/.config/opencode/agents}"
|
|
ACTIVE_CLAUDE="${OPC_AGENTS_CLAUDE_ACTIVE:-$HOME/.claude/agents}"
|
|
ACTIVE_FEYNMAN="${OPC_AGENTS_FEYNMAN_ACTIVE:-$HOME/.feynman/agent/agents}"
|
|
|
|
# Index lives under opencode-archive (canonical) but tracks ALL targets
|
|
INDEX_JSON="$PARKED_OPENCODE/INDEX.json"
|
|
INDEX_MD="$PARKED_OPENCODE/INDEX.md"
|
|
|
|
DEFAULT_TARGETS="${OPC_AGENTS_TARGETS:-opencode,claude,feynman}"
|
|
|
|
# resolve target → parked dir / active dir
|
|
target_parked() {
|
|
case "$1" in
|
|
opencode) printf '%s\n' "$PARKED_OPENCODE" ;;
|
|
claude) printf '%s\n' "$PARKED_CLAUDE" ;;
|
|
feynman) printf '%s\n' "$PARKED_FEYNMAN" ;;
|
|
*) echo "unknown target: $1" >&2; return 1 ;;
|
|
esac
|
|
}
|
|
target_active() {
|
|
case "$1" in
|
|
opencode) printf '%s\n' "$ACTIVE_OPENCODE" ;;
|
|
claude) printf '%s\n' "$ACTIVE_CLAUDE" ;;
|
|
feynman) printf '%s\n' "$ACTIVE_FEYNMAN" ;;
|
|
*) echo "unknown target: $1" >&2; return 1 ;;
|
|
esac
|
|
}
|
|
|
|
resolve_targets() {
|
|
local raw="${1:-$DEFAULT_TARGETS}"
|
|
case "$raw" in
|
|
all) raw="opencode,claude,feynman" ;;
|
|
both) raw="opencode,claude" ;;
|
|
esac
|
|
printf '%s\n' "$raw" | tr ',' '\n' | awk 'NF' | awk '!seen[$0]++'
|
|
}
|
|
|
|
parse_target_flag() {
|
|
REMAINING_ARGS=()
|
|
PARSED_TARGETS=""
|
|
local seen_target=""
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--target=*) seen_target="${1#--target=}"; shift ;;
|
|
--target|-t) seen_target="${2:-}"; shift 2 ;;
|
|
*) REMAINING_ARGS+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
if [ -n "$seen_target" ]; then
|
|
PARSED_TARGETS=$(resolve_targets "$seen_target" | paste -sd ',')
|
|
else
|
|
PARSED_TARGETS=$(resolve_targets | paste -sd ',')
|
|
fi
|
|
}
|
|
|
|
require_dirs() {
|
|
[ -d "$PARKED_OPENCODE" ] || mkdir -p "$PARKED_OPENCODE"
|
|
[ -d "$PARKED_CLAUDE" ] || mkdir -p "$PARKED_CLAUDE"
|
|
[ -d "$PARKED_FEYNMAN" ] || mkdir -p "$PARKED_FEYNMAN"
|
|
mkdir -p "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" "$ACTIVE_FEYNMAN"
|
|
}
|
|
|
|
# legacy aliases for backward compat
|
|
PARKED="$PARKED_OPENCODE"
|
|
ACTIVE="$ACTIVE_OPENCODE"
|
|
|
|
# list agent base-names (without .md) in a given dir
|
|
agent_names_in() {
|
|
local base="$1"
|
|
[ -d "$base" ] || return 0
|
|
find "$base" -mindepth 1 -maxdepth 1 -type f -name '*.md' \
|
|
! -name 'INDEX.md' ! -name 'README.md' -printf '%f\n' \
|
|
| sed 's/\.md$//' | sort
|
|
}
|
|
|
|
cmd_status() {
|
|
require_dirs
|
|
echo "Parked sources (per-target — different formats):"
|
|
printf " %-10s %5d (%s)\n" "opencode" "$(agent_names_in "$PARKED_OPENCODE" | wc -l)" "$PARKED_OPENCODE"
|
|
printf " %-10s %5d (%s)\n" "claude" "$(agent_names_in "$PARKED_CLAUDE" | wc -l)" "$PARKED_CLAUDE"
|
|
printf " %-10s %5d (%s)\n" "feynman" "$(agent_names_in "$PARKED_FEYNMAN" | wc -l)" "$PARKED_FEYNMAN"
|
|
|
|
echo ""
|
|
echo "Active targets:"
|
|
local t base n
|
|
for t in opencode claude feynman; do
|
|
base=$(target_active "$t")
|
|
n=$(agent_names_in "$base" | wc -l)
|
|
printf " %-10s %5d (%s)\n" "$t" "$n" "$base"
|
|
done
|
|
|
|
# mode breakdown (opencode-style frontmatter; claude format has no `mode:`)
|
|
local active_oc; active_oc=$(agent_names_in "$ACTIVE_OPENCODE" | wc -l)
|
|
if [ "$active_oc" -gt 0 ]; then
|
|
local primary subagent other
|
|
primary=$(grep -l '^mode: primary' "$ACTIVE_OPENCODE"/*.md 2>/dev/null | wc -l)
|
|
subagent=$(grep -l '^mode: subagent' "$ACTIVE_OPENCODE"/*.md 2>/dev/null | wc -l)
|
|
other=$((active_oc - primary - subagent))
|
|
echo
|
|
echo "opencode mode breakdown:"
|
|
printf " primary : %5d\n" "$primary"
|
|
printf " subagent : %5d\n" "$subagent"
|
|
[ "$other" -gt 0 ] && printf " (other) : %5d\n" "$other" || true
|
|
fi
|
|
}
|
|
|
|
cmd_list() {
|
|
require_dirs
|
|
local which="${1:-parked}"
|
|
case "$which" in
|
|
active) agent_names_in "$ACTIVE" ;;
|
|
parked) agent_names_in "$PARKED" ;;
|
|
all) { agent_names_in "$ACTIVE"; agent_names_in "$PARKED"; } | sort -u ;;
|
|
*) echo "usage: opc-agents list {active|parked|all}" >&2; exit 2 ;;
|
|
esac
|
|
}
|
|
|
|
cmd_categories() {
|
|
require_dirs
|
|
agent_names_in "$PARKED" \
|
|
| awk -F'-' '{print $1}' \
|
|
| sort | uniq -c | sort -rn
|
|
}
|
|
|
|
ensure_fzf() {
|
|
command -v fzf >/dev/null || { echo "fzf is required for this command" >&2; exit 1; }
|
|
}
|
|
|
|
ensure_jq() {
|
|
command -v jq >/dev/null || { echo "jq is required for this command" >&2; exit 1; }
|
|
}
|
|
|
|
# suffix_of <name> — everything after the first '-' (the variant); empty for bare base
|
|
suffix_of() {
|
|
local n="$1"
|
|
case "$n" in
|
|
*-*) printf '%s\n' "${n#*-}" ;;
|
|
*) printf '%s\n' "" ;;
|
|
esac
|
|
}
|
|
|
|
# variant suffix distribution across PARKED
|
|
cmd_variants() {
|
|
require_dirs
|
|
agent_names_in "$PARKED" | while read -r n; do
|
|
local s; s=$(suffix_of "$n")
|
|
[ -n "$s" ] && printf '%s\n' "$s" || printf '%s\n' "<base>"
|
|
done | sort | uniq -c | sort -rn
|
|
}
|
|
|
|
# bulk enable agents matching variant suffix (within PARKED)
|
|
cmd_enable_variant() {
|
|
require_dirs
|
|
local v="${1:-}"
|
|
[ -n "$v" ] || { echo "usage: opc-agents enable-variant <suffix>" >&2; exit 2; }
|
|
local -a matches=()
|
|
while IFS= read -r n; do
|
|
[ -z "$n" ] && continue
|
|
[ "$(suffix_of "$n")" = "$v" ] && matches+=("$n")
|
|
done < <(agent_names_in "$PARKED")
|
|
[ "${#matches[@]}" -gt 0 ] || { echo "no parked agents with variant: $v" >&2; exit 1; }
|
|
echo "Will enable ${#matches[@]} agent(s) with variant '$v':"
|
|
printf ' %s\n' "${matches[@]}"
|
|
local n
|
|
for n in "${matches[@]}"; do cmd_enable "$n" || true; done
|
|
}
|
|
|
|
# bulk disable from ACTIVE matching variant suffix
|
|
cmd_disable_variant() {
|
|
require_dirs
|
|
local v="${1:-}"
|
|
[ -n "$v" ] || { echo "usage: opc-agents disable-variant <suffix>" >&2; exit 2; }
|
|
local -a matches=()
|
|
while IFS= read -r n; do
|
|
[ -z "$n" ] && continue
|
|
[ "$(suffix_of "$n")" = "$v" ] && matches+=("$n")
|
|
done < <(agent_names_in "$ACTIVE")
|
|
[ "${#matches[@]}" -gt 0 ] || { echo "no active agents with variant: $v" >&2; exit 1; }
|
|
echo "Will disable ${#matches[@]} agent(s) with variant '$v':"
|
|
printf ' %s\n' "${matches[@]}"
|
|
local n
|
|
for n in "${matches[@]}"; do cmd_disable "$n" || true; done
|
|
}
|
|
|
|
# live scan: emit agent base-names whose description matches Domain: <wanted>
|
|
# (INDEX-free, so it stays correct between reindexes)
|
|
domain_scan_in() {
|
|
local base="$1" wanted="$2"
|
|
[ -d "$base" ] || return 0
|
|
grep -liE "^description:.*[Dd]omain:[[:space:]]*${wanted}([^A-Za-z]|$)" \
|
|
"$base"/*.md 2>/dev/null \
|
|
| while read -r f; do
|
|
local b; b=$(basename "$f" .md)
|
|
case "$b" in INDEX|README) ;; *) printf '%s\n' "$b" ;; esac
|
|
done | sort -u
|
|
}
|
|
|
|
# bulk enable agents whose 'domain' matches (live scan, no INDEX dependency)
|
|
cmd_enable_domain() {
|
|
require_dirs
|
|
local d="${1:-}"
|
|
[ -n "$d" ] || { echo "usage: opc-agents enable-domain <domain>" >&2; exit 2; }
|
|
local -a matches=()
|
|
mapfile -t matches < <(domain_scan_in "$PARKED" "$d")
|
|
[ "${#matches[@]}" -gt 0 ] || { echo "no parked agents in domain: $d" >&2; exit 1; }
|
|
echo "Will enable ${#matches[@]} agent(s) in domain '$d':"
|
|
printf ' %s\n' "${matches[@]}"
|
|
local n
|
|
for n in "${matches[@]}"; do cmd_enable "$n" || true; done
|
|
}
|
|
|
|
# bulk disable from ACTIVE matching domain (live scan)
|
|
cmd_disable_domain() {
|
|
require_dirs
|
|
local d="${1:-}"
|
|
[ -n "$d" ] || { echo "usage: opc-agents disable-domain <domain>" >&2; exit 2; }
|
|
local -a matches=()
|
|
mapfile -t matches < <(domain_scan_in "$ACTIVE" "$d")
|
|
[ "${#matches[@]}" -gt 0 ] || { echo "no active agents in domain: $d" >&2; exit 1; }
|
|
echo "Will disable ${#matches[@]} agent(s) in domain '$d':"
|
|
printf ' %s\n' "${matches[@]}"
|
|
local n
|
|
for n in "${matches[@]}"; do cmd_disable "$n" || true; done
|
|
}
|
|
|
|
# enable one agent in selected targets
|
|
# usage: cmd_enable [--target T] <name>
|
|
cmd_enable() {
|
|
parse_target_flag "$@"
|
|
set -- "${REMAINING_ARGS[@]}"
|
|
require_dirs
|
|
local name="${1:-}"
|
|
[ -n "$name" ] || { echo "usage: opc-agents enable [--target opencode|claude|feynman|all] <name>" >&2; exit 2; }
|
|
name="${name%.md}"
|
|
local t pdir adir src dst
|
|
IFS=',' read -ra tgts <<< "$PARSED_TARGETS"
|
|
for t in "${tgts[@]}"; do
|
|
pdir=$(target_parked "$t") || continue
|
|
adir=$(target_active "$t") || continue
|
|
src="$pdir/$name.md"
|
|
dst="$adir/$name.md"
|
|
if [ ! -f "$src" ]; then
|
|
echo "[$t] not parked (no source for this target): $name"
|
|
continue
|
|
fi
|
|
if [ -f "$dst" ]; then
|
|
echo "[$t] already active: $name"
|
|
else
|
|
cp "$src" "$dst"
|
|
echo "[$t] enabled: $name"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# disable one agent in selected targets (keeps parked source untouched)
|
|
cmd_disable() {
|
|
parse_target_flag "$@"
|
|
set -- "${REMAINING_ARGS[@]}"
|
|
require_dirs
|
|
local name="${1:-}"
|
|
[ -n "$name" ] || { echo "usage: opc-agents disable [--target opencode|claude|feynman|all] <name>" >&2; exit 2; }
|
|
name="${name%.md}"
|
|
local t adir src
|
|
IFS=',' read -ra tgts <<< "$PARSED_TARGETS"
|
|
for t in "${tgts[@]}"; do
|
|
adir=$(target_active "$t") || continue
|
|
src="$adir/$name.md"
|
|
if [ ! -f "$src" ]; then
|
|
echo "[$t] not active: $name"
|
|
continue
|
|
fi
|
|
rm -f "$src"
|
|
echo "[$t] disabled: $name"
|
|
done
|
|
}
|
|
|
|
# disable all active agents across selected targets
|
|
# usage: cmd_disable_all [--target T] [-y|--yes] [--keep-primary]
|
|
cmd_disable_all() {
|
|
parse_target_flag "$@"
|
|
set -- "${REMAINING_ARGS[@]}"
|
|
require_dirs
|
|
local force=0 keep_primary=0 a
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
-y|--yes) force=1 ;;
|
|
--keep-primary) keep_primary=1 ;;
|
|
*) echo "unknown flag: $a" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
IFS=',' read -ra tgts <<< "$PARSED_TARGETS"
|
|
# Collect (target, name) pairs that should be disabled
|
|
local -a pairs=()
|
|
local t adir n
|
|
for t in "${tgts[@]}"; do
|
|
adir=$(target_active "$t") || continue
|
|
while IFS= read -r n; do
|
|
[ -z "$n" ] && continue
|
|
if [ "$keep_primary" -eq 1 ]; then
|
|
# only opencode has 'mode: primary' frontmatter; claude format has no mode field → never primary
|
|
if grep -q '^mode: primary' "$adir/$n.md" 2>/dev/null; then continue; fi
|
|
fi
|
|
pairs+=("$t:$n")
|
|
done < <(agent_names_in "$adir")
|
|
done
|
|
[ "${#pairs[@]}" -gt 0 ] || { echo "no active agents to disable in [${tgts[*]}]"; exit 0; }
|
|
echo "Will disable ${#pairs[@]} (target:agent) entries:"
|
|
printf ' %s\n' "${pairs[@]}"
|
|
if [ "$force" -ne 1 ]; then
|
|
printf "Proceed? [y/N] "
|
|
read -r ans </dev/tty || ans=""
|
|
case "$ans" in y|Y|yes|YES) ;; *) echo "cancelled"; exit 0 ;; esac
|
|
fi
|
|
for p in "${pairs[@]}"; do
|
|
t="${p%%:*}"; n="${p#*:}"
|
|
adir=$(target_active "$t")
|
|
[ -f "$adir/$n.md" ] && rm -f "$adir/$n.md" && echo "[$t] disabled: $n"
|
|
done
|
|
}
|
|
|
|
# enable every parked agent into selected target(s) — symmetric to disable-all
|
|
# usage: cmd_enable_all [--target T] [-y|--yes]
|
|
# Per-target: only reads from THAT target's parked source (format-aware).
|
|
# Idempotent: skips agents already active.
|
|
cmd_enable_all() {
|
|
parse_target_flag "$@"
|
|
set -- "${REMAINING_ARGS[@]}"
|
|
require_dirs
|
|
local force=0
|
|
if [ "${1:-}" = "-y" ] || [ "${1:-}" = "--yes" ]; then
|
|
force=1
|
|
fi
|
|
IFS=',' read -ra tgts <<< "$PARSED_TARGETS"
|
|
local -a pairs=()
|
|
local t pdir adir n
|
|
for t in "${tgts[@]}"; do
|
|
pdir=$(target_parked "$t") || continue
|
|
adir=$(target_active "$t") || continue
|
|
while IFS= read -r n; do
|
|
[ -z "$n" ] && continue
|
|
[ -f "$adir/$n.md" ] && continue # already active
|
|
pairs+=("$t:$n")
|
|
done < <(agent_names_in "$pdir")
|
|
done
|
|
[ "${#pairs[@]}" -gt 0 ] || { echo "all parked agents already active in [${tgts[*]}]"; exit 0; }
|
|
echo "Will enable ${#pairs[@]} (target:agent) entries:"
|
|
printf ' %s\n' "${pairs[@]:0:20}"
|
|
[ "${#pairs[@]}" -gt 20 ] && echo " ... and $((${#pairs[@]} - 20)) more"
|
|
if [ "$force" -ne 1 ]; then
|
|
printf "Proceed? [y/N] "
|
|
read -r ans </dev/tty || ans=""
|
|
case "$ans" in y|Y|yes|YES) ;; *) echo "cancelled"; exit 0 ;; esac
|
|
fi
|
|
local p enabled=0
|
|
for p in "${pairs[@]}"; do
|
|
t="${p%%:*}"; n="${p#*:}"
|
|
pdir=$(target_parked "$t"); adir=$(target_active "$t")
|
|
[ -f "$pdir/$n.md" ] || continue
|
|
[ -f "$adir/$n.md" ] && continue
|
|
cp "$pdir/$n.md" "$adir/$n.md"
|
|
enabled=$((enabled+1))
|
|
done
|
|
echo "enabled $enabled agent(s)."
|
|
}
|
|
|
|
# enable category (by prefix)
|
|
cmd_enable_category() {
|
|
require_dirs
|
|
ensure_fzf
|
|
local prefix="${1:-}"
|
|
[ -n "$prefix" ] || { echo "usage: opc-agents enable-category <prefix>" >&2; exit 2; }
|
|
local -a matches
|
|
mapfile -t matches < <(agent_names_in "$PARKED" | awk -v p="$prefix" '$0 ~ "^" p "(-|$)"')
|
|
[ "${#matches[@]}" -gt 0 ] || { echo "no parked agents match prefix: $prefix" >&2; exit 1; }
|
|
echo "Matches (${#matches[@]}):"
|
|
local selection
|
|
selection=$(printf '%s\n' "${matches[@]}" | fzf --multi --height=70% \
|
|
--prompt="enable-category $prefix > " \
|
|
--header="TAB: toggle | ENTER: confirm | ESC: cancel" \
|
|
--preview="sed -n '1,40p' \"$PARKED\"/{}.md 2>/dev/null" \
|
|
--preview-window=right:60%:wrap)
|
|
[ -n "$selection" ] || { echo "cancelled"; exit 0; }
|
|
local n
|
|
while IFS= read -r n; do
|
|
[ -z "$n" ] && continue
|
|
cmd_enable "$n" || true
|
|
done <<< "$selection"
|
|
}
|
|
|
|
# disable category (by prefix, only ACTIVE)
|
|
cmd_disable_category() {
|
|
require_dirs
|
|
ensure_fzf
|
|
local prefix="${1:-}"
|
|
[ -n "$prefix" ] || { echo "usage: opc-agents disable-category <prefix>" >&2; exit 2; }
|
|
local -a matches
|
|
mapfile -t matches < <(agent_names_in "$ACTIVE" | awk -v p="$prefix" '$0 ~ "^" p "(-|$)"')
|
|
[ "${#matches[@]}" -gt 0 ] || { echo "no active agents match prefix: $prefix" >&2; exit 1; }
|
|
echo "Active matches (${#matches[@]}):"
|
|
local selection
|
|
selection=$(printf '%s\n' "${matches[@]}" | fzf --multi --height=70% \
|
|
--prompt="disable-category $prefix > " \
|
|
--header="TAB: toggle | ENTER: confirm | ESC: cancel" \
|
|
--preview="sed -n '1,40p' \"$ACTIVE\"/{}.md 2>/dev/null" \
|
|
--preview-window=right:60%:wrap)
|
|
[ -n "$selection" ] || { echo "cancelled"; exit 0; }
|
|
local n
|
|
while IFS= read -r n; do
|
|
[ -z "$n" ] && continue
|
|
cmd_disable "$n" || true
|
|
done <<< "$selection"
|
|
}
|
|
|
|
# interactive target picker — returns comma-separated targets on stdout, exit 1 on cancel.
|
|
pick_targets() {
|
|
local action="${1:-enable}"
|
|
ensure_fzf
|
|
local oc cl fy
|
|
oc=$(agent_names_in "$ACTIVE_OPENCODE" | wc -l)
|
|
cl=$(agent_names_in "$ACTIVE_CLAUDE" | wc -l)
|
|
fy=$(agent_names_in "$ACTIVE_FEYNMAN" | wc -l)
|
|
local sel
|
|
sel=$(printf '%s\n' \
|
|
"all → opencode + claude + feynman" \
|
|
"opencode ($oc active)" \
|
|
"claude ($cl active)" \
|
|
"feynman ($fy active)" \
|
|
| fzf --multi --height=30% \
|
|
--prompt="$action target > " \
|
|
--header="TAB: multi-select | ENTER: confirm (default highlighted = all)" \
|
|
| awk '{print $1}')
|
|
[ -n "$sel" ] || return 1
|
|
if grep -qx all <<< "$sel"; then
|
|
echo "opencode,claude,feynman"
|
|
else
|
|
echo "$sel" | paste -sd ','
|
|
fi
|
|
}
|
|
|
|
# Helper: union of parked agent names across all 3 targets (deduped)
|
|
parked_union() {
|
|
{ agent_names_in "$PARKED_OPENCODE"
|
|
agent_names_in "$PARKED_CLAUDE"
|
|
agent_names_in "$PARKED_FEYNMAN"; } | sort -u
|
|
}
|
|
|
|
# Helper: union of active agent names with target indicators "name\t[targets]"
|
|
active_union_with_targets() {
|
|
{
|
|
for t in opencode claude feynman; do
|
|
while IFS= read -r n; do
|
|
[ -z "$n" ] && continue
|
|
printf '%s\t%s\n' "$n" "$t"
|
|
done < <(agent_names_in "$(target_active "$t")")
|
|
done
|
|
} | awk -F'\t' '
|
|
{ agents[$1] = (agents[$1] ? agents[$1]"," : "") $2 }
|
|
END { for (a in agents) printf "%s\t[%s]\n", a, agents[a] }
|
|
' | sort
|
|
}
|
|
|
|
# Resolve a parked source path for an agent name (first target where parked file exists)
|
|
parked_path_for() {
|
|
local name="$1" t pdir
|
|
for t in opencode claude feynman; do
|
|
pdir=$(target_parked "$t")
|
|
[ -f "$pdir/$name.md" ] && { echo "$pdir/$name.md"; return 0; }
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# interactive pick: union-parked category → multi-select → pick target(s)
|
|
cmd_pick() {
|
|
require_dirs
|
|
ensure_fzf
|
|
# Categories from union of parked across all 3 targets
|
|
local cats category
|
|
cats=$(parked_union | awk -F'-' '{print $1}' | sort | uniq -c | sort -rn)
|
|
category=$(printf '%s\n' "$cats" | fzf --prompt="category > " --height=50% \
|
|
--header="Pick a category (first column = count) • parked = union of all 3 targets" \
|
|
| awk '{print $2}')
|
|
[ -n "$category" ] || { echo "cancelled"; exit 0; }
|
|
|
|
# Build {name}\t{parked-in:[oc,cl,fy]} lines
|
|
local tmp; tmp=$(mktemp)
|
|
{
|
|
for t in opencode claude feynman; do
|
|
while IFS= read -r n; do
|
|
[ -z "$n" ] && continue
|
|
printf '%s\t%s\n' "$n" "$t"
|
|
done < <(agent_names_in "$(target_parked "$t")")
|
|
done
|
|
} | awk -F'\t' '
|
|
{ p[$1] = (p[$1] ? p[$1]"," : "") $2 }
|
|
END { for (a in p) printf "%s\t[%s]\n", a, p[a] }
|
|
' | awk -F'\t' -v p="$category" '$1 ~ "^"p"(-|$)"' | sort > "$tmp"
|
|
|
|
local selection
|
|
selection=$(fzf --multi --height=80% \
|
|
--prompt="$category > " \
|
|
--header="TAB: toggle | ENTER: confirm → pick target" \
|
|
--delimiter='\t' --with-nth=1,2 \
|
|
--preview="for d in \"$PARKED_OPENCODE\" \"$PARKED_CLAUDE\" \"$PARKED_FEYNMAN\"; do [ -f \"\$d\"/{1}.md ] && { echo \"=== \$d ===\"; sed -n '1,40p' \"\$d\"/{1}.md; break; }; done 2>/dev/null" \
|
|
--preview-window=right:60%:wrap < "$tmp")
|
|
rm -f "$tmp"
|
|
[ -n "$selection" ] || { echo "cancelled"; exit 0; }
|
|
|
|
local targets
|
|
targets=$(pick_targets "enable") || { echo "cancelled (no target)"; exit 0; }
|
|
|
|
local n
|
|
while IFS=$'\t' read -r n _; do
|
|
[ -z "$n" ] && continue
|
|
cmd_enable --target "$targets" "$n" || true
|
|
done <<< "$selection"
|
|
}
|
|
|
|
# interactive disable-pick: union of actives across all 3 targets → pick target(s) to remove from
|
|
cmd_disable_pick() {
|
|
require_dirs
|
|
ensure_fzf
|
|
local tmp; tmp=$(mktemp)
|
|
active_union_with_targets > "$tmp"
|
|
[ -s "$tmp" ] || { rm -f "$tmp"; echo "no active agents across any target"; exit 0; }
|
|
|
|
local cats category
|
|
cats=$(awk -F'\t' '{print $1}' "$tmp" | awk -F'-' '{print $1}' | sort | uniq -c | sort -rn)
|
|
category=$(printf '%s\n' "$cats" | fzf --prompt="active-category > " --height=50% \
|
|
--header="Pick category from ACTIVE agents (any target)" \
|
|
| awk '{print $2}')
|
|
[ -n "$category" ] || { rm -f "$tmp"; echo "cancelled"; exit 0; }
|
|
|
|
local selection
|
|
selection=$(awk -F'\t' -v p="$category" '$1 ~ "^"p"(-|$)"' "$tmp" \
|
|
| fzf --multi --height=80% \
|
|
--prompt="disable $category > " \
|
|
--header="TAB: toggle | ENTER: pick target(s) to disable from" \
|
|
--delimiter='\t' --with-nth=1,2 \
|
|
--preview="for d in \"$ACTIVE_OPENCODE\" \"$ACTIVE_CLAUDE\" \"$ACTIVE_FEYNMAN\"; do [ -f \"\$d\"/{1}.md ] && { echo \"=== \$d ===\"; sed -n '1,40p' \"\$d\"/{1}.md; break; }; done 2>/dev/null" \
|
|
--preview-window=right:60%:wrap)
|
|
rm -f "$tmp"
|
|
[ -n "$selection" ] || { echo "cancelled"; exit 0; }
|
|
|
|
local targets
|
|
targets=$(pick_targets "disable") || { echo "cancelled (no target)"; exit 0; }
|
|
|
|
local n
|
|
while IFS=$'\t' read -r n _; do
|
|
[ -z "$n" ] && continue
|
|
cmd_disable --target "$targets" "$n" || true
|
|
done <<< "$selection"
|
|
}
|
|
|
|
# fzf search (parked) by name+description (uses INDEX.json)
|
|
cmd_search() {
|
|
require_dirs
|
|
ensure_fzf
|
|
command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; }
|
|
[ -f "$INDEX_JSON" ] || { echo "INDEX.json missing — run: opc-agents reindex" >&2; exit 1; }
|
|
local query="${1:-}"
|
|
local tmp; tmp=$(mktemp)
|
|
jq -r '.[] | "\(.name)\t\(.mode)\t\(.domain // "-")\t\(.variant // "-")\t\(.description)"' "$INDEX_JSON" > "$tmp"
|
|
local selection
|
|
if [ -n "$query" ]; then
|
|
selection=$(fzf --query="$query" --multi --height=80% \
|
|
--delimiter='\t' --with-nth=1,2,3,4,5 \
|
|
--prompt="search > " \
|
|
--header="TAB: toggle | ENTER: enable selected" \
|
|
--preview="sed -n '1,40p' \"$PARKED\"/{1}.md 2>/dev/null || echo '(not parked — maybe already active)'" \
|
|
--preview-window=right:60%:wrap < "$tmp")
|
|
else
|
|
selection=$(fzf --multi --height=80% \
|
|
--delimiter='\t' --with-nth=1,2,3,4,5 \
|
|
--prompt="search > " \
|
|
--header="TAB: toggle | ENTER: enable selected" \
|
|
--preview="sed -n '1,40p' \"$PARKED\"/{1}.md 2>/dev/null || echo '(not parked — maybe already active)'" \
|
|
--preview-window=right:60%:wrap < "$tmp")
|
|
fi
|
|
rm -f "$tmp"
|
|
[ -n "$selection" ] || { echo "cancelled"; exit 0; }
|
|
local name
|
|
while IFS=$'\t' read -r name _ _ _ _; do
|
|
[ -z "$name" ] && continue
|
|
cmd_enable "$name" || true
|
|
done <<< "$selection"
|
|
}
|
|
|
|
# fzf search (ACTIVE only) by name+description
|
|
cmd_disable_search() {
|
|
require_dirs
|
|
ensure_fzf
|
|
command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; }
|
|
[ -f "$INDEX_JSON" ] || { echo "INDEX.json missing — run: opc-agents reindex" >&2; exit 1; }
|
|
local query="${1:-}"
|
|
local tmp; tmp=$(mktemp)
|
|
jq -r '.[] | select(.status=="active" or .status=="both") | "\(.name)\t\(.mode)\t\(.domain // "-")\t\(.variant // "-")\t\(.description)"' "$INDEX_JSON" > "$tmp"
|
|
[ -s "$tmp" ] || { rm -f "$tmp"; echo "no active agents indexed (try: opc-agents reindex)"; exit 0; }
|
|
local selection
|
|
if [ -n "$query" ]; then
|
|
selection=$(fzf --query="$query" --multi --height=80% \
|
|
--delimiter='\t' --with-nth=1,2,3,4,5 \
|
|
--prompt="disable-search > " \
|
|
--header="TAB: toggle | ENTER: disable selected" \
|
|
--preview="sed -n '1,40p' \"$ACTIVE\"/{1}.md 2>/dev/null" \
|
|
--preview-window=right:60%:wrap < "$tmp")
|
|
else
|
|
selection=$(fzf --multi --height=80% \
|
|
--delimiter='\t' --with-nth=1,2,3,4,5 \
|
|
--prompt="disable-search > " \
|
|
--header="TAB: toggle | ENTER: disable selected" \
|
|
--preview="sed -n '1,40p' \"$ACTIVE\"/{1}.md 2>/dev/null" \
|
|
--preview-window=right:60%:wrap < "$tmp")
|
|
fi
|
|
rm -f "$tmp"
|
|
[ -n "$selection" ] || { echo "cancelled"; exit 0; }
|
|
local name
|
|
while IFS=$'\t' read -r name _ _ _ _; do
|
|
[ -z "$name" ] && continue
|
|
cmd_disable "$name" || true
|
|
done <<< "$selection"
|
|
}
|
|
|
|
# rebuild INDEX.json + INDEX.md by scanning parked + active
|
|
cmd_reindex() {
|
|
require_dirs
|
|
python3 - "$PARKED_OPENCODE" "$PARKED_CLAUDE" "$PARKED_FEYNMAN" "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" "$ACTIVE_FEYNMAN" "$INDEX_JSON" "$INDEX_MD" <<'PY'
|
|
import sys, re, json
|
|
from pathlib import Path
|
|
|
|
parked_oc = Path(sys.argv[1])
|
|
parked_cl = Path(sys.argv[2])
|
|
parked_fy = Path(sys.argv[3])
|
|
active_oc = Path(sys.argv[4])
|
|
active_cl = Path(sys.argv[5])
|
|
active_fy = Path(sys.argv[6])
|
|
out_json = Path(sys.argv[7])
|
|
out_md = Path(sys.argv[8])
|
|
|
|
def read_agent(p: Path):
|
|
name = p.stem
|
|
desc, mode, domain, variant = "", "", "", ""
|
|
persona = name.split("-", 1)[0]
|
|
suffix = name[len(persona)+1:] if "-" in name else ""
|
|
try:
|
|
txt = p.read_text(errors="replace")
|
|
m = re.match(r"^---\s*\n(.*?)\n---\s*\n", txt, re.DOTALL)
|
|
if m:
|
|
fm = m.group(1)
|
|
d = re.search(r'^description:\s*"?(.+?)"?\s*$', fm, re.MULTILINE | re.DOTALL)
|
|
mo = re.search(r'^mode:\s*"?([^"\n]+?)"?\s*$', fm, re.MULTILINE)
|
|
if d: desc = d.group(1).strip().splitlines()[0]
|
|
if mo: mode = mo.group(1).strip()
|
|
except Exception as e:
|
|
desc = f"(read error: {e})"
|
|
dm = re.search(r"Domain:\s*([A-Za-z]+)", desc)
|
|
if dm: domain = dm.group(1).lower()
|
|
vm = re.search(r"Variant:\s*([A-Za-z0-9][A-Za-z0-9-]*)", desc)
|
|
if vm: variant = vm.group(1).lower()
|
|
elif suffix: variant = suffix
|
|
return {
|
|
"name": name, "persona": persona, "variant": variant,
|
|
"mode": mode or "?", "domain": domain,
|
|
"description": desc,
|
|
}
|
|
|
|
agents = {}
|
|
def upsert(rec):
|
|
n = rec["name"]
|
|
cur = agents.get(n)
|
|
if cur is None:
|
|
rec.setdefault("parked", {"opencode": False, "claude": False, "feynman": False})
|
|
rec.setdefault("active", [])
|
|
agents[n] = rec
|
|
else:
|
|
# prefer opencode-side description (richer with permissions/mode); fill any blanks
|
|
for k in ("description","domain","variant"):
|
|
if not cur.get(k) and rec.get(k): cur[k] = rec[k]
|
|
if (cur.get("mode") in ("","?")) and rec.get("mode") not in ("","?"):
|
|
cur["mode"] = rec["mode"]
|
|
|
|
def scan_parked(base: Path, target: str):
|
|
if not base.exists(): return
|
|
for p in sorted(base.glob("*.md")):
|
|
if p.name in ("INDEX.md","README.md"): continue
|
|
rec = read_agent(p)
|
|
upsert(rec)
|
|
agents[rec["name"]]["parked"][target] = True
|
|
|
|
def scan_active(base: Path, target: str):
|
|
if not base.exists(): return
|
|
for p in sorted(base.glob("*.md")):
|
|
if p.name in ("INDEX.md","README.md"): continue
|
|
rec = read_agent(p)
|
|
upsert(rec)
|
|
if target not in agents[rec["name"]]["active"]:
|
|
agents[rec["name"]]["active"].append(target)
|
|
|
|
scan_parked(parked_oc, "opencode")
|
|
scan_parked(parked_cl, "claude")
|
|
scan_parked(parked_fy, "feynman")
|
|
scan_active(active_oc, "opencode")
|
|
scan_active(active_cl, "claude")
|
|
scan_active(active_fy, "feynman")
|
|
|
|
items = sorted(agents.values(), key=lambda x: x["name"])
|
|
out_json.write_text(json.dumps(items, ensure_ascii=False, indent=2))
|
|
|
|
n_oc_park = sum(1 for i in items if i["parked"]["opencode"])
|
|
n_cl_park = sum(1 for i in items if i["parked"]["claude"])
|
|
n_fy_park = sum(1 for i in items if i["parked"]["feynman"])
|
|
in_oc = sum(1 for i in items if "opencode" in i["active"])
|
|
in_cl = sum(1 for i in items if "claude" in i["active"])
|
|
in_fy = sum(1 for i in items if "feynman" in i["active"])
|
|
all_park = sum(1 for i in items if all(i["parked"].get(t) for t in ("opencode","claude","feynman")))
|
|
prim = sum(1 for i in items if i["mode"] == "primary")
|
|
sub = sum(1 for i in items if i["mode"] == "subagent")
|
|
|
|
from collections import Counter
|
|
dom_c = Counter(i["domain"] for i in items if i["domain"])
|
|
var_c = Counter(i["variant"] for i in items if i["variant"])
|
|
top_doms = ", ".join(f"{k}:{v}" for k,v in dom_c.most_common(8)) or "-"
|
|
top_vars = ", ".join(f"{k}:{v}" for k,v in var_c.most_common(8)) or "-"
|
|
|
|
lines = [
|
|
"# Opencode + Claude + Feynman Agents — Index",
|
|
"",
|
|
f"**{len(items)} unique agents.**",
|
|
f"Parked sources — opencode: {n_oc_park}, claude: {n_cl_park}, feynman: {n_fy_park}, all-three: {all_park}.",
|
|
f"Active — opencode: {in_oc}, claude: {in_cl}, feynman: {in_fy}.",
|
|
f"Modes — primary: {prim}, subagent: {sub}.",
|
|
f"Top domains — {top_doms}.",
|
|
f"Top variants — {top_vars}.",
|
|
"",
|
|
f"Parked opencode: `{parked_oc}`",
|
|
f"Parked claude : `{parked_cl}`",
|
|
f"Parked feynman : `{parked_fy}`",
|
|
f"Active opencode: `{active_oc}`",
|
|
f"Active claude : `{active_cl}`",
|
|
f"Active feynman : `{active_fy}`",
|
|
"",
|
|
"| # | Parked | Active | Mode | Persona | Variant | Domain | Name | Description |",
|
|
"|---|--------|--------|------|---------|---------|--------|------|-------------|",
|
|
]
|
|
for i, it in enumerate(items, 1):
|
|
d = it["description"].replace("\n", " ").replace("|", "\\|")
|
|
if len(d) > 140: d = d[:137] + "..."
|
|
pk = ",".join(t for t in ("opencode","claude","feynman") if it["parked"][t]) or "-"
|
|
ac = ",".join(it.get("active", [])) or "-"
|
|
lines.append(
|
|
f"| {i} | {pk} | {ac} | {it['mode']} | {it['persona']} | "
|
|
f"{it['variant'] or '-'} | {it['domain'] or '-'} | `{it['name']}` | {d} |"
|
|
)
|
|
out_md.write_text("\n".join(lines) + "\n")
|
|
print(f"reindexed: {len(items)} agents "
|
|
f"(parked oc={n_oc_park} cl={n_cl_park} fy={n_fy_park} all3={all_park}, "
|
|
f"active oc={in_oc} cl={in_cl} fy={in_fy}, primary={prim}, subagent={sub})")
|
|
PY
|
|
}
|
|
|
|
usage() {
|
|
cat <<'USG'
|
|
opc-agents — multi-target agent manager (opencode + claude + feynman)
|
|
|
|
Targets: opencode, claude, feynman ("all"=all three, "both"=opencode+claude legacy)
|
|
Default = opencode,claude,feynman. Use --target / -t to scope ops.
|
|
|
|
status per-target counts (parked + active) + mode breakdown
|
|
list {active|parked|all} list agent names (active = opencode set)
|
|
categories prefix-based base-persona counts (PARKED opencode)
|
|
variants suffix-based variant counts (PARKED opencode)
|
|
enable [--target T] <name> copy parked → active in selected targets
|
|
disable [--target T] <name> remove from active in selected targets
|
|
enable-all [--target T] [-y|--yes] enable every parked agent into selected targets
|
|
(idempotent — restore-from-archive helper)
|
|
disable-all [--target T] [-y|--yes] [--keep-primary]
|
|
disable every active agent across targets
|
|
enable-category <prefix> fzf multi-pick within a base-persona prefix, then enable
|
|
disable-category <prefix> fzf multi-pick of ACTIVE agents with prefix, then disable
|
|
enable-variant <suffix> bulk enable all *-<suffix>.md (eg. salva)
|
|
disable-variant <suffix> bulk disable all *-<suffix>.md
|
|
enable-domain <domain> bulk enable agents whose `Domain: <x>` matches (live scan)
|
|
disable-domain <domain> bulk disable from ACTIVE matching domain
|
|
pick fzf: choose category → multi-select → enable
|
|
disable-pick fzf: choose ACTIVE category → multi-select → disable
|
|
search [query] fzf fuzzy search across all index fields
|
|
disable-search [query] fzf fuzzy search ACTIVE agents only (disable)
|
|
reindex rebuild INDEX.json / INDEX.md (per-target parked + active)
|
|
|
|
Notes:
|
|
- Agent format differs between targets:
|
|
opencode → `permission: {...}` + `mode:` field
|
|
claude → `tools: Read, Glob, ...` (PascalCase)
|
|
feynman → `tools: read, write, ...` (lowercase) + `thinking`/`output` fields
|
|
Each target reads from its own parked dir; cross-target enable copies as-is.
|
|
- disable* never deletes parked sources.
|
|
- --keep-primary skips agents with `mode: primary` (opencode-format only).
|
|
- Single source-of-truth: ~/Documents/personas/agents-{opencode,claude,feynman}-archive
|
|
|
|
Environment:
|
|
OPC_AGENTS_PARKED opencode parked (default: personas/agents-opencode-archive)
|
|
OPC_AGENTS_CLAUDE_PARKED claude parked (default: personas/agents-claude-archive)
|
|
OPC_AGENTS_FEYNMAN_PARKED feynman parked (default: personas/agents-feynman-archive)
|
|
OPC_AGENTS_ACTIVE opencode active (default: ~/.config/opencode/agents)
|
|
OPC_AGENTS_CLAUDE_ACTIVE claude active (default: ~/.claude/agents)
|
|
OPC_AGENTS_FEYNMAN_ACTIVE feynman active (default: ~/.feynman/agent/agents)
|
|
OPC_AGENTS_TARGETS default targets when --target omitted
|
|
(default: opencode,claude,feynman)
|
|
USG
|
|
}
|
|
|
|
main() {
|
|
local cmd="${1:-}"
|
|
[ -n "$cmd" ] || { usage; exit 2; }
|
|
shift || true
|
|
case "$cmd" in
|
|
status) cmd_status "$@" ;;
|
|
list) cmd_list "$@" ;;
|
|
categories|cats) cmd_categories "$@" ;;
|
|
variants|vars) cmd_variants "$@" ;;
|
|
enable) cmd_enable "$@" ;;
|
|
disable) cmd_disable "$@" ;;
|
|
enable-all) cmd_enable_all "$@" ;;
|
|
disable-all) cmd_disable_all "$@" ;;
|
|
enable-category|enable-cat) cmd_enable_category "$@" ;;
|
|
disable-category|disable-cat) cmd_disable_category "$@" ;;
|
|
enable-variant|enable-var) cmd_enable_variant "$@" ;;
|
|
disable-variant|disable-var) cmd_disable_variant "$@" ;;
|
|
enable-domain) cmd_enable_domain "$@" ;;
|
|
disable-domain) cmd_disable_domain "$@" ;;
|
|
pick) cmd_pick "$@" ;;
|
|
disable-pick) cmd_disable_pick "$@" ;;
|
|
search) cmd_search "$@" ;;
|
|
disable-search) cmd_disable_search "$@" ;;
|
|
reindex) cmd_reindex "$@" ;;
|
|
-h|--help|help) usage ;;
|
|
*) echo "unknown command: $cmd" >&2; usage; exit 2 ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|