From aaeb31f3ca279282cab1e6966cc39ed1ed7c54b4 Mon Sep 17 00:00:00 2001 From: salva Date: Thu, 30 Apr 2026 22:01:56 +0300 Subject: [PATCH] feat: variant + domain bulk ops, enriched INDEX schema - variants : suffix distribution counts (salva, iran, russian-doctrine, ...) - enable-variant / disable-variant : bulk by filename suffix - enable-domain / disable-domain : bulk by frontmatter Domain (live scan) - INDEX.json now exposes persona, variant, domain - INDEX.md columns expanded; top-domains and top-variants summaries - search/disable-search now also match domain & variant - regex fix: 'c2-hunting' was being parsed as 'c' (digit not allowed) --- README.md | 37 +++++++--- bin/opc-agents | 178 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 183 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 8ca7c73..8b270c6 100644 --- a/README.md +++ b/README.md @@ -33,21 +33,38 @@ Override via env: ## Commands +### Inspection + +| | | +|---|---| +| `status` | counts of active vs parked + primary/subagent breakdown | +| `list {active\|parked\|all}` | list agent names | +| `categories` / `cats` | prefix-based base-persona counts (PARKED) | +| `variants` / `vars` | suffix-based variant counts (PARKED) — eg. `salva`, `iran` | + +### Single-agent operations + | | | |---|---| -| `status` | counts of active vs parked + mode breakdown | -| `list {active\|parked\|all}` | list agent names | -| `categories` / `cats` | prefix-based category counts (PARKED) | | `enable ` | enable single agent (copy parked → active) | | `disable ` | disable single agent (remove from active; keep parked) | | `disable-all [-y\|--yes] [--keep-primary]` | disable every active agent (asks for confirmation) | -| `enable-category ` | fzf multi-pick within a prefix, then enable | -| `disable-category ` | fzf multi-pick of ACTIVE agents with prefix, then disable | -| `pick` | fzf: choose category → multi-select → enable | -| `disable-pick` | fzf: choose ACTIVE category → multi-select → disable | -| `search [query]` | fzf fuzzy search across name+description (enable) | -| `disable-search [query]` | fzf fuzzy search ACTIVE agents only (disable) | -| `reindex` | rebuild `INDEX.json` / `INDEX.md` | + +### Bulk by axis + +| | | +|---|---| +| `enable-category ` / `disable-category ` | fzf multi-pick by **base persona** prefix (eg. `frodo`, `marshal`) | +| `enable-variant ` / `disable-variant ` | bulk by **variant suffix** (eg. `salva`, `iran`, `russian-doctrine`) | +| `enable-domain ` / `disable-domain ` | bulk by `Domain:` value in description (live scan) | + +### Interactive / search + +| | | +|---|---| +| `pick` / `disable-pick` | fzf: pick category → multi-select | +| `search [query]` / `disable-search` | fzf fuzzy search across name + mode + domain + variant + description | +| `reindex` | rebuild `INDEX.json` / `INDEX.md` (extracts persona/variant/domain from frontmatter) | `fzf` is required for the interactive pickers; `jq` for the search variants; `python3` for `reindex`. diff --git a/bin/opc-agents b/bin/opc-agents index a3829d1..7a63b69 100755 --- a/bin/opc-agents +++ b/bin/opc-agents @@ -69,6 +69,103 @@ 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 — 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' "" + 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 " >&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 " >&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: +# (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 " >&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 " >&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 by base-name (no .md) cmd_enable() { require_dirs @@ -244,18 +341,18 @@ cmd_search() { [ -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\(.description)"' "$INDEX_JSON" > "$tmp" + 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 \ + --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 \ + --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)'" \ @@ -264,7 +361,7 @@ cmd_search() { rm -f "$tmp" [ -n "$selection" ] || { echo "cancelled"; exit 0; } local name - while IFS=$'\t' read -r name _ _; do + while IFS=$'\t' read -r name _ _ _ _; do [ -z "$name" ] && continue cmd_enable "$name" || true done <<< "$selection" @@ -278,19 +375,19 @@ cmd_disable_search() { [ -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\(.description)"' "$INDEX_JSON" > "$tmp" + 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 \ + --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 \ + --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" \ @@ -299,7 +396,7 @@ cmd_disable_search() { rm -f "$tmp" [ -n "$selection" ] || { echo "cancelled"; exit 0; } local name - while IFS=$'\t' read -r name _ _; do + while IFS=$'\t' read -r name _ _ _ _; do [ -z "$name" ] && continue cmd_disable "$name" || true done <<< "$selection" @@ -319,31 +416,46 @@ out_md = Path(sys.argv[4]) def read_agent(p: Path): name = p.stem - desc = "" - mode = "" + 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) + 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})" - return name, desc, mode + # Domain & Variant from description (allow digits and hyphens, eg. "c2-hunting") + 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 # fallback to filename suffix + return { + "name": name, "persona": persona, "variant": variant, + "mode": mode or "?", "domain": domain, + "description": desc, + } seen = {} def scan(base: Path, status: str): if not base.exists(): return for p in sorted(base.glob("*.md")): if p.name in ("INDEX.md", "README.md"): continue - n, d, m = read_agent(p) + rec = read_agent(p) + n = rec["name"] if n in seen: seen[n]["status"] = "both" else: - seen[n] = {"name": n, "mode": m or "?", "description": d, "status": status} + rec["status"] = status + seen[n] = rec scan(active, "active") scan(parked, "parked") @@ -356,21 +468,33 @@ parked_n = sum(1 for i in items if i["status"] in ("parked", "both")) prim = sum(1 for i in items if i["mode"] == "primary") sub = sum(1 for i in items if i["mode"] == "subagent") +# domain & variant breakdowns +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)) +top_vars = ", ".join(f"{k}:{v}" for k,v in var_c.most_common(8)) + lines = [ "# Opencode Agents — Index", "", f"**{len(items)} unique agents** — active: {active_n}, parked: {parked_n}.", f"Modes — primary: {prim}, subagent: {sub}.", + f"Top domains — {top_doms}.", + f"Top variants — {top_vars}.", "", f"Active: `{active}` | Parked: `{parked}`", "", - "| # | Status | Mode | Name | Description |", - "|---|--------|------|------|-------------|", + "| # | Status | Mode | Persona | Variant | Domain | Name | Description |", + "|---|--------|------|---------|---------|--------|------|-------------|", ] for i, it in enumerate(items, 1): d = it["description"].replace("\n", " ").replace("|", "\\|") - if len(d) > 200: d = d[:197] + "..." - lines.append(f"| {i} | {it['status']} | {it['mode']} | `{it['name']}` | {d} |") + if len(d) > 160: d = d[:157] + "..." + lines.append( + f"| {i} | {it['status']} | {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 (active={active_n}, parked={parked_n}, primary={prim}, subagent={sub})") PY @@ -382,18 +506,23 @@ opc-agents — opencode agent manager (file-based: .md) status counts of active vs parked (+ mode breakdown) list {active|parked|all} list agent names - categories prefix-based category counts (PARKED) + categories prefix-based base-persona counts (PARKED) + variants suffix-based variant counts (PARKED) — eg. salva, iran enable enable single agent (copy parked → active) disable disable single agent (remove from active; keep parked) disable-all [-y|--yes] [--keep-primary] disable every active agent (asks for confirmation) - enable-category fzf multi-pick within a prefix, then enable + enable-category fzf multi-pick within a base-persona prefix, then enable disable-category fzf multi-pick of ACTIVE agents with prefix, then disable + enable-variant bulk enable all *-.md from PARKED (eg. salva) + disable-variant bulk disable all *-.md from ACTIVE + enable-domain bulk enable agents whose `Domain: ` matches (live scan) + disable-domain bulk disable from ACTIVE matching domain (live scan) pick fzf: choose category → multi-select → enable disable-pick fzf: choose ACTIVE category → multi-select → disable - search [query] fzf fuzzy search across name+description (enable) + search [query] fzf fuzzy search (name+mode+domain+variant+description) disable-search [query] fzf fuzzy search ACTIVE agents only (disable) - reindex rebuild INDEX.json / INDEX.md + reindex rebuild INDEX.json / INDEX.md (extracts persona/variant/domain) Notes: - Each agent is a single .md file (frontmatter + body). @@ -414,11 +543,16 @@ main() { status) cmd_status "$@" ;; list) cmd_list "$@" ;; categories|cats) cmd_categories "$@" ;; + variants|vars) cmd_variants "$@" ;; enable) cmd_enable "$@" ;; disable) cmd_disable "$@" ;; 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 "$@" ;;