feat: multi-target (opencode + claude) with per-target parked sources
- opencode and claude use different agent frontmatter formats (permission vs tools)
so opc-agents keeps a SEPARATE parked source per target.
- New env vars: OPC_AGENTS_CLAUDE_PARKED, OPC_AGENTS_CLAUDE_ACTIVE,
OPC_AGENTS_TARGETS. Existing OPC_AGENTS_PARKED still works (opencode side).
- enable/disable/disable-all accept --target {opencode|claude|both} (default both).
- Reindex scans both parked sources + both active dirs; INDEX exposes
parked.{opencode,claude} (whether source available) and active list.
- INDEX.md table includes Parked/Active columns per agent.
- Default parked locations moved to personas/agents-{opencode,claude}-archive
with backward-compat symlink at ~/Documents/opencode-agents-parked.
This commit is contained in:
36
README.md
36
README.md
@@ -1,12 +1,12 @@
|
||||
# opc-agents
|
||||
|
||||
Small shell utility for toggling opencode **agents** between a curated "active" set and a full "parked" catalog. Keeps `~/.config/opencode/agents/` small and intentional while preserving the full agent library offline.
|
||||
|
||||
Companion to [`opc-skills`](https://gitea.taygun.net.tr/salvacybersec/opc-skills) — same UX, file-based unit (each agent is a single `<name>.md` file rather than a folder).
|
||||
**Multi-target** agent manager for **opencode** + **Claude Code**. Per-target parked sources (different formats), shared CLI surface. Companion to [`opc-skills`](https://gitea.taygun.net.tr/salvacybersec/opc-skills).
|
||||
|
||||
## Why
|
||||
|
||||
opencode injects every agent's frontmatter (`description`, `mode`, …) into the calling agent's tool registry. With 100+ agents the registry alone burns ~280K tokens of context before the user even types a prompt. Park the ones you don't need this week.
|
||||
opencode injects every agent's frontmatter into the calling agent's tool registry. With 100+ agents the registry alone burns ~280K tokens of context before the user types a prompt. Park the ones you don't need this week.
|
||||
|
||||
opencode and Claude Code use **different agent frontmatter formats** (opencode: `permission: { … }`, claude: `tools: Read, Glob, …`), so opc-agents keeps a separate parked source per target.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -19,17 +19,29 @@ Ensure `~/.local/bin` is on your `PATH`.
|
||||
## Layout it expects
|
||||
|
||||
```
|
||||
~/Documents/opencode-agents-parked/ # full catalog — untracked data dir
|
||||
<name>.md
|
||||
INDEX.json
|
||||
INDEX.md
|
||||
~/.config/opencode/agents/ # only currently-enabled agents
|
||||
<name>.md
|
||||
~/Documents/personas/agents-opencode-archive/ # parked, opencode format
|
||||
<name>.md (frontmatter: mode, permission, …)
|
||||
INDEX.json + INDEX.md (canonical index — also tracks claude side)
|
||||
|
||||
~/Documents/personas/agents-claude-archive/ # parked, claude format
|
||||
<name>.md (frontmatter: tools: Read, Glob, …)
|
||||
|
||||
~/.config/opencode/agents/ # active, opencode target
|
||||
~/.claude/agents/ # active, claude target
|
||||
```
|
||||
|
||||
(`~/Documents/opencode-agents-parked` is symlinked to `personas/agents-opencode-archive` for backward compat.)
|
||||
|
||||
Override via env:
|
||||
- `OPC_AGENTS_PARKED` — parked catalog root (default `~/Documents/opencode-agents-parked`)
|
||||
- `OPC_AGENTS_ACTIVE` — active agents root (default `~/.config/opencode/agents`)
|
||||
- `OPC_AGENTS_PARKED` — opencode parked source (default `personas/agents-opencode-archive`)
|
||||
- `OPC_AGENTS_CLAUDE_PARKED` — claude parked source (default `personas/agents-claude-archive`)
|
||||
- `OPC_AGENTS_ACTIVE` — opencode active dir (default `~/.config/opencode/agents`)
|
||||
- `OPC_AGENTS_CLAUDE_ACTIVE` — claude active dir (default `~/.claude/agents`)
|
||||
- `OPC_AGENTS_TARGETS` — comma-separated default targets (default `opencode,claude`)
|
||||
|
||||
## Targets
|
||||
|
||||
`enable`, `disable`, and `disable-all` accept `--target` / `-t` (`opencode`, `claude`, `both`/`all`). Default is `opencode,claude`. An agent missing in a target's parked source is skipped with a clear message — there is **no automatic format conversion**.
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
358
bin/opc-agents
358
bin/opc-agents
@@ -1,22 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# opc-agents — opencode agent enable/disable manager
|
||||
# Parked: ~/Documents/opencode-agents-parked
|
||||
# Active: ~/.config/opencode/agents
|
||||
# opc-agents — multi-target agent enable/disable manager (opencode + claude)
|
||||
#
|
||||
# Mirrors opc-skills, but each unit is a single <name>.md file (not a folder).
|
||||
# Parked sources (per-target — formats differ):
|
||||
# opencode → ~/Documents/personas/agents-opencode-archive (permission: { ... })
|
||||
# claude → ~/Documents/personas/agents-claude-archive (tools: Read, Glob, ...)
|
||||
#
|
||||
# Active targets:
|
||||
# opencode → ~/.config/opencode/agents
|
||||
# claude → ~/.claude/agents
|
||||
#
|
||||
# Default targets: opencode,claude (override via OPC_AGENTS_TARGETS or --target)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PARKED="${OPC_AGENTS_PARKED:-$HOME/Documents/opencode-agents-parked}"
|
||||
ACTIVE="${OPC_AGENTS_ACTIVE:-$HOME/.config/opencode/agents}"
|
||||
INDEX_JSON="$PARKED/INDEX.json"
|
||||
INDEX_MD="$PARKED/INDEX.md"
|
||||
# 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}"
|
||||
ACTIVE_OPENCODE="${OPC_AGENTS_ACTIVE:-$HOME/.config/opencode/agents}"
|
||||
ACTIVE_CLAUDE="${OPC_AGENTS_CLAUDE_ACTIVE:-$HOME/.claude/agents}"
|
||||
|
||||
# Index lives under opencode-archive (canonical) but tracks BOTH targets
|
||||
INDEX_JSON="$PARKED_OPENCODE/INDEX.json"
|
||||
INDEX_MD="$PARKED_OPENCODE/INDEX.md"
|
||||
|
||||
DEFAULT_TARGETS="${OPC_AGENTS_TARGETS:-opencode,claude}"
|
||||
|
||||
# resolve target → parked dir / active dir
|
||||
target_parked() {
|
||||
case "$1" in
|
||||
opencode) printf '%s\n' "$PARKED_OPENCODE" ;;
|
||||
claude) printf '%s\n' "$PARKED_CLAUDE" ;;
|
||||
*) 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" ;;
|
||||
*) echo "unknown target: $1" >&2; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_targets() {
|
||||
local raw="${1:-$DEFAULT_TARGETS}"
|
||||
case "$raw" in both|all) 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" ] || { echo "parked dir missing: $PARKED" >&2; exit 1; }
|
||||
mkdir -p "$ACTIVE"
|
||||
[ -d "$PARKED_OPENCODE" ] || mkdir -p "$PARKED_OPENCODE"
|
||||
[ -d "$PARKED_CLAUDE" ] || mkdir -p "$PARKED_CLAUDE"
|
||||
mkdir -p "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE"
|
||||
}
|
||||
|
||||
# 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"
|
||||
@@ -28,19 +86,28 @@ agent_names_in() {
|
||||
|
||||
cmd_status() {
|
||||
require_dirs
|
||||
local active parked
|
||||
active=$(agent_names_in "$ACTIVE" | wc -l)
|
||||
parked=$(agent_names_in "$PARKED" | wc -l)
|
||||
printf "active : %5d (%s)\n" "$active" "$ACTIVE"
|
||||
printf "parked : %5d (%s)\n" "$parked" "$PARKED"
|
||||
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"
|
||||
|
||||
# mode breakdown for active (primary vs subagent) — primary stays in main context
|
||||
if [ "$active" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Active targets:"
|
||||
local t base n
|
||||
for t in opencode claude; 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"/*.md 2>/dev/null | wc -l)
|
||||
subagent=$(grep -l '^mode: subagent' "$ACTIVE"/*.md 2>/dev/null | wc -l)
|
||||
other=$((active - primary - subagent))
|
||||
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"
|
||||
@@ -166,38 +233,62 @@ cmd_disable_domain() {
|
||||
for n in "${matches[@]}"; do cmd_disable "$n" || true; done
|
||||
}
|
||||
|
||||
# enable one agent by base-name (no .md)
|
||||
# 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 <name>" >&2; exit 2; }
|
||||
[ -n "$name" ] || { echo "usage: opc-agents enable [--target opencode|claude|both] <name>" >&2; exit 2; }
|
||||
name="${name%.md}"
|
||||
local src="$PARKED/$name.md" dst="$ACTIVE/$name.md"
|
||||
[ -f "$src" ] || { echo "not parked: $name" >&2; exit 1; }
|
||||
[ -f "$dst" ] && { echo "already active: $name"; exit 0; }
|
||||
cp "$src" "$dst"
|
||||
echo "enabled: $name"
|
||||
}
|
||||
|
||||
# disable one active agent (keep parked copy)
|
||||
cmd_disable() {
|
||||
require_dirs
|
||||
local name="${1:-}"
|
||||
[ -n "$name" ] || { echo "usage: opc-agents disable <name>" >&2; exit 2; }
|
||||
name="${name%.md}"
|
||||
local src="$ACTIVE/$name.md" dst="$PARKED/$name.md"
|
||||
[ -f "$src" ] || { echo "not active: $name" >&2; exit 1; }
|
||||
if [ -f "$dst" ]; then
|
||||
rm -f "$src"
|
||||
echo "removed active copy (parked version kept): $name"
|
||||
else
|
||||
mv "$src" "$dst"
|
||||
echo "disabled (moved to parked): $name"
|
||||
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 all active agents
|
||||
# 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|both] <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
|
||||
@@ -207,27 +298,33 @@ cmd_disable_all() {
|
||||
*) echo "unknown flag: $a" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
local -a actives
|
||||
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
|
||||
mapfile -t actives < <(
|
||||
agent_names_in "$ACTIVE" | while read -r n; do
|
||||
grep -q '^mode: primary' "$ACTIVE/$n.md" 2>/dev/null || echo "$n"
|
||||
done
|
||||
)
|
||||
else
|
||||
mapfile -t actives < <(agent_names_in "$ACTIVE")
|
||||
# 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
|
||||
[ "${#actives[@]}" -gt 0 ] || { echo "no active agents to disable"; exit 0; }
|
||||
echo "Will disable ${#actives[@]} active agent(s):"
|
||||
printf ' %s\n' "${actives[@]}"
|
||||
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
|
||||
local n
|
||||
for n in "${actives[@]}"; do
|
||||
cmd_disable "$n" || true
|
||||
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
|
||||
}
|
||||
|
||||
@@ -405,14 +502,16 @@ cmd_disable_search() {
|
||||
# rebuild INDEX.json + INDEX.md by scanning parked + active
|
||||
cmd_reindex() {
|
||||
require_dirs
|
||||
python3 - "$PARKED" "$ACTIVE" "$INDEX_JSON" "$INDEX_MD" <<'PY'
|
||||
python3 - "$PARKED_OPENCODE" "$PARKED_CLAUDE" "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" "$INDEX_JSON" "$INDEX_MD" <<'PY'
|
||||
import sys, re, json
|
||||
from pathlib import Path
|
||||
|
||||
parked = Path(sys.argv[1])
|
||||
active = Path(sys.argv[2])
|
||||
out_json = Path(sys.argv[3])
|
||||
out_md = Path(sys.argv[4])
|
||||
parked_oc = Path(sys.argv[1])
|
||||
parked_cl = Path(sys.argv[2])
|
||||
active_oc = Path(sys.argv[3])
|
||||
active_cl = Path(sys.argv[4])
|
||||
out_json = Path(sys.argv[5])
|
||||
out_md = Path(sys.argv[6])
|
||||
|
||||
def read_agent(p: Path):
|
||||
name = p.stem
|
||||
@@ -430,108 +529,145 @@ def read_agent(p: Path):
|
||||
if mo: mode = mo.group(1).strip()
|
||||
except Exception as e:
|
||||
desc = f"(read error: {e})"
|
||||
# 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
|
||||
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,
|
||||
}
|
||||
|
||||
seen = {}
|
||||
def scan(base: Path, status: str):
|
||||
agents = {}
|
||||
def upsert(rec):
|
||||
n = rec["name"]
|
||||
cur = agents.get(n)
|
||||
if cur is None:
|
||||
rec.setdefault("parked", {"opencode": False, "claude": 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)
|
||||
n = rec["name"]
|
||||
if n in seen:
|
||||
seen[n]["status"] = "both"
|
||||
else:
|
||||
rec["status"] = status
|
||||
seen[n] = rec
|
||||
upsert(rec)
|
||||
agents[rec["name"]]["parked"][target] = True
|
||||
|
||||
scan(active, "active")
|
||||
scan(parked, "parked")
|
||||
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)
|
||||
|
||||
items = sorted(seen.values(), key=lambda x: x["name"])
|
||||
scan_parked(parked_oc, "opencode")
|
||||
scan_parked(parked_cl, "claude")
|
||||
scan_active(active_oc, "opencode")
|
||||
scan_active(active_cl, "claude")
|
||||
|
||||
items = sorted(agents.values(), key=lambda x: x["name"])
|
||||
out_json.write_text(json.dumps(items, ensure_ascii=False, indent=2))
|
||||
|
||||
active_n = sum(1 for i in items if i["status"] in ("active", "both"))
|
||||
parked_n = sum(1 for i in items if i["status"] in ("parked", "both"))
|
||||
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"])
|
||||
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"])
|
||||
both_park = sum(1 for i in items if i["parked"]["opencode"] and i["parked"]["claude"])
|
||||
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))
|
||||
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 Agents — Index",
|
||||
"# Opencode + Claude Agents — Index",
|
||||
"",
|
||||
f"**{len(items)} unique agents** — active: {active_n}, parked: {parked_n}.",
|
||||
f"**{len(items)} unique agents.**",
|
||||
f"Parked sources — opencode: {n_oc_park}, claude: {n_cl_park}, both: {both_park}.",
|
||||
f"Active — opencode: {in_oc}, claude: {in_cl}.",
|
||||
f"Modes — primary: {prim}, subagent: {sub}.",
|
||||
f"Top domains — {top_doms}.",
|
||||
f"Top variants — {top_vars}.",
|
||||
"",
|
||||
f"Active: `{active}` | Parked: `{parked}`",
|
||||
f"Parked opencode: `{parked_oc}`",
|
||||
f"Parked claude : `{parked_cl}`",
|
||||
f"Active opencode: `{active_oc}`",
|
||||
f"Active claude : `{active_cl}`",
|
||||
"",
|
||||
"| # | Status | Mode | Persona | Variant | Domain | Name | Description |",
|
||||
"|---|--------|------|---------|---------|--------|------|-------------|",
|
||||
"| # | Parked | Active | Mode | Persona | Variant | Domain | Name | Description |",
|
||||
"|---|--------|--------|------|---------|---------|--------|------|-------------|",
|
||||
]
|
||||
for i, it in enumerate(items, 1):
|
||||
d = it["description"].replace("\n", " ").replace("|", "\\|")
|
||||
if len(d) > 160: d = d[:157] + "..."
|
||||
if len(d) > 140: d = d[:137] + "..."
|
||||
pk = ",".join(t for t in ("opencode","claude") if it["parked"][t]) or "-"
|
||||
ac = ",".join(it.get("active", [])) or "-"
|
||||
lines.append(
|
||||
f"| {i} | {it['status']} | {it['mode']} | {it['persona']} | "
|
||||
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 (active={active_n}, parked={parked_n}, primary={prim}, subagent={sub})")
|
||||
print(f"reindexed: {len(items)} agents "
|
||||
f"(parked oc={n_oc_park} cl={n_cl_park} both={both_park}, "
|
||||
f"active oc={in_oc} cl={in_cl}, primary={prim}, subagent={sub})")
|
||||
PY
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USG'
|
||||
opc-agents — opencode agent manager (file-based: <name>.md)
|
||||
opc-agents — multi-target agent manager (opencode + claude)
|
||||
|
||||
status counts of active vs parked (+ mode breakdown)
|
||||
list {active|parked|all} list agent names
|
||||
categories prefix-based base-persona counts (PARKED)
|
||||
variants suffix-based variant counts (PARKED) — eg. salva, iran
|
||||
enable <name> enable single agent (copy parked → active)
|
||||
disable <name> disable single agent (remove from active; keep parked)
|
||||
disable-all [-y|--yes] [--keep-primary]
|
||||
disable every active agent (asks for confirmation)
|
||||
Targets: opencode, claude (or "both"/"all"; default = opencode,claude)
|
||||
Use --target / -t with enable/disable/disable-all to scope the operation.
|
||||
|
||||
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
|
||||
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 from PARKED (eg. salva)
|
||||
disable-variant <suffix> bulk disable all *-<suffix>.md from ACTIVE
|
||||
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 (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 (name+mode+domain+variant+description)
|
||||
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 (extracts persona/variant/domain)
|
||||
reindex rebuild INDEX.json / INDEX.md (per-target parked + active)
|
||||
|
||||
Notes:
|
||||
- Each agent is a single <name>.md file (frontmatter + body).
|
||||
- disable* never deletes data — removes active copy and keeps (or restores) parked.
|
||||
- --keep-primary on disable-all skips agents with `mode: primary` (those stay loaded).
|
||||
- Agent format differs between targets: opencode uses `permission: {...}`,
|
||||
claude uses `tools: Read, Glob, ...`. Each target reads from its own parked dir.
|
||||
- disable* never deletes parked sources.
|
||||
- --keep-primary skips agents with `mode: primary` (opencode-format only).
|
||||
- Single source-of-truth: ~/Documents/personas/agents-{opencode,claude}-archive
|
||||
|
||||
Environment:
|
||||
OPC_AGENTS_PARKED override parked dir (default: ~/Documents/opencode-agents-parked)
|
||||
OPC_AGENTS_ACTIVE override active dir (default: ~/.config/opencode/agents)
|
||||
OPC_AGENTS_PARKED opencode parked (default: personas/agents-opencode-archive)
|
||||
OPC_AGENTS_CLAUDE_PARKED claude parked (default: personas/agents-claude-archive)
|
||||
OPC_AGENTS_ACTIVE opencode active (default: ~/.config/opencode/agents)
|
||||
OPC_AGENTS_CLAUDE_ACTIVE claude active (default: ~/.claude/agents)
|
||||
OPC_AGENTS_TARGETS default targets when --target omitted (default: opencode,claude)
|
||||
USG
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user