feat: feynman target + interactive target picker + fzf preview fix

- Add feynman as third target (~/.feynman/agent/skills) alongside opencode
  and claude. Default targets now: opencode,claude,feynman.
- Fix fzf {} preview shell-escape bug: {} in fzf is shell-escaped (wrapped
  in single quotes), so "$PARKED/{}/SKILL.md" embedded literal quotes into
  the path and broke preview. Switched to "$PARKED"/{}/SKILL.md (variable
  quoted, {} unquoted) so bash concatenation yields a clean path.
- pick: after skill multi-select, prompt target picker (all / opencode /
  claude / feynman, TAB for multi). ENTER on highlighted "all" = all three.
- 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 output with all-three intersection counter.
- reindex: tracks per-target active arrays for all 3.
- New env vars: OPC_FEYNMAN_ACTIVE.
This commit is contained in:
salvacybersec
2026-05-01 13:13:55 +03:00
parent f53f9320a8
commit 44daf2b969
2 changed files with 154 additions and 66 deletions

View File

@@ -1,10 +1,10 @@
# opc-skills
**Multi-target** skill manager for **opencode** + **Claude Code**. Single source-of-truth (parked archive in the personas repo), per-target lazy activation. Default operations target both platforms.
**Multi-target** skill manager for **opencode** + **Claude Code** + **Feynman**. Single source-of-truth (parked archive in the personas repo), per-target lazy activation. Default operations target all three platforms; an interactive target-picker prompts when picking via fzf.
## Why
opencode (≥1.14) auto-scans `~/.claude/skills/` as well as its own `~/.config/opencode/skills/` — so a 1000-skill Claude Code archive ends up bloating the opencode prompt. opc-skills inverts this: keep both target dirs *empty by default*, copy individual skills into them on demand.
opencode (≥1.14) auto-scans `~/.claude/skills/` as well as its own `~/.config/opencode/skills/` — so a 1000-skill Claude Code archive ends up bloating the opencode prompt. Feynman has its own `~/.feynman/agent/skills/`. opc-skills inverts this: keep all three target dirs *empty by default*, copy individual skills into them on demand.
For full agent / skill / persona generation across multiple AI platforms, see the upstream [`personas`](https://gitea.taygun.net.tr/salvacybersec/personas) repo.
@@ -29,6 +29,9 @@ Ensure `~/.local/bin` is on your `PATH`.
~/.claude/skills/ # currently-enabled (claude target)
<skill-name>/SKILL.md
~/.feynman/agent/skills/ # currently-enabled (feynman target)
<skill-name>/SKILL.md
```
(`~/Documents/opencode-skills-parked` is symlinked to `personas/skills-archive` for backward-compat.)
@@ -37,7 +40,8 @@ Override via env:
- `OPC_PARKED` — parked catalog root (default `~/Documents/personas/skills-archive`)
- `OPC_ACTIVE` — opencode active dir (default `~/.config/opencode/skills`)
- `OPC_CLAUDE_ACTIVE` — claude active dir (default `~/.claude/skills`)
- `OPC_TARGETS` — comma-separated default targets (default `opencode,claude`)
- `OPC_FEYNMAN_ACTIVE` — feynman active dir (default `~/.feynman/agent/skills`)
- `OPC_TARGETS` — comma-separated default targets (default `opencode,claude,feynman`)
## Targets
@@ -47,16 +51,19 @@ Override via env:
|---|---|
| `--target opencode` | only `~/.config/opencode/skills/` |
| `--target claude` | only `~/.claude/skills/` |
| `--target both` (or `all`) | both — also the default |
| `--target feynman` | only `~/.feynman/agent/skills/` |
| `--target all` | all three |
| `--target both` | legacy `opencode,claude` (no feynman) |
| `--target opencode,feynman` | comma-separated subset |
When omitted, the default is taken from `OPC_TARGETS` (default `opencode,claude`).
When omitted, the default is taken from `OPC_TARGETS` (default `opencode,claude,feynman`).
## Commands
### Inspection
```
opc-skills status # parked + per-target active counts
opc-skills status # parked + per-target active counts (3 targets)
opc-skills list {active|parked|all} # skill folder names
opc-skills categories # verb-prefix counts (performing, detecting, ...)
opc-skills domains # frontmatter `domain:` distribution
@@ -88,27 +95,42 @@ opc-skills disable-tag [--all] <tag>...
### Interactive / search
```
opc-skills pick # fzf: category → multi-select → enable
opc-skills disable-pick # fzf: ACTIVE category → multi-select → disable
opc-skills search [query] # fzf fuzzy over name+description
opc-skills disable-search [query] # fzf over ACTIVE only
opc-skills pick # fzf: category → multi-select skills → target picker → enable
opc-skills disable-pick # fzf: union-of-actives → category → multi-select → target picker → disable
opc-skills search [query] # fzf fuzzy over name+description (enables on selection)
opc-skills disable-search [query] # fzf over ACTIVE only (disables on selection)
opc-skills reindex # rebuild INDEX.json / INDEX.md
```
#### `pick` flow
1. Choose a verb-prefix category (`performing`, `detecting`, …)
2. fzf shows skills in that category — `TAB` toggles, `ENTER` confirms
3. **Target picker** appears: `all` / `opencode` / `claude` / `feynman``TAB` for multi-select; `ENTER` on highlighted `all` (default) sends to all three
4. Selected skills are enabled in the chosen target(s)
#### `disable-pick` flow
Now multi-target aware: lists the **union of all active skills across the three targets**, with each entry annotated `[oc,cl,fy]` showing where it's currently active. After multi-select, the same target picker decides which target(s) to remove from.
`fzf` is required for the interactive commands; `jq` for the bulk-by-axis commands; `python3` (with optional `PyYAML`) for `reindex`.
### INDEX schema
`reindex` parses each `SKILL.md` frontmatter and emits records with: `folder`, `name`, `description`, `domain`, `subdomain`, `tags[]`, `status`. `enable` / `disable` perform an incremental status update on `INDEX.json` so `enable-domain` / `disable-tag` etc. stay accurate without a full reindex.
`reindex` parses each `SKILL.md` frontmatter and emits records with: `folder`, `name`, `description`, `domain`, `subdomain`, `tags[]`, `active[]` (per-target list — `opencode`/`claude`/`feynman`), `status`. `enable` / `disable` perform an incremental status update on `INDEX.json` so `enable-domain` / `disable-tag` etc. stay accurate without a full reindex.
## Shared reference files
Some skills reference sibling files via `../<name>.md` (e.g. the 20 Feynman research skills use `../_platform-mapping.md` for cross-platform tool mapping). `opc-skills enable` auto-syncs any such files from PARKED → ACTIVE so the relative references keep resolving.
Some skills reference sibling files via `../<name>.md` (e.g. the 20 Feynman research skills use `../_platform-mapping.md` for cross-platform tool mapping). `opc-skills enable` auto-syncs any such files from PARKED → ACTIVE in **all three targets** so the relative references keep resolving.
Currently synced:
- `_platform-mapping.md` — Feynman-skills cross-platform subagent/scheduling/persistence mapping
If you add new shared-reference files at the PARKED root, extend `sync_shared_refs()` in `bin/opc-skills`.
If you add new shared-reference files at the PARKED root, extend `sync_shared_refs_to_targets()` in `bin/opc-skills`.
## Implementation notes
- `fzf` placeholder `{}` is shell-escaped automatically — preview commands use `"$PARKED"/{}/SKILL.md` (variable quoted, `{}` unquoted) so bash concatenation produces a clean path. Wrapping `{}` in extra quotes (`"$PARKED/{}/SKILL.md"`) embeds fzf's escape literals into the path and breaks preview.
## Known interaction

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# opc-skills — multi-target skill enable/disable manager (opencode + claude)
# opc-skills — multi-target skill enable/disable manager (opencode + claude + feynman)
#
# Parked (single source-of-truth):
# ~/Documents/personas/skills-archive (default; symlinked from old location)
@@ -7,34 +7,38 @@
# Active targets (where enable copies to):
# opencode → ~/.config/opencode/skills
# claude → ~/.claude/skills
# feynman → ~/.feynman/agent/skills
#
# Default targets when --target is omitted: opencode,claude (set OPC_TARGETS to override)
# Default targets when --target is omitted: opencode,claude,feynman (set OPC_TARGETS to override)
set -euo pipefail
PARKED="${OPC_PARKED:-$HOME/Documents/personas/skills-archive}"
ACTIVE_OPENCODE="${OPC_ACTIVE:-$HOME/.config/opencode/skills}"
ACTIVE_CLAUDE="${OPC_CLAUDE_ACTIVE:-$HOME/.claude/skills}"
ACTIVE_FEYNMAN="${OPC_FEYNMAN_ACTIVE:-$HOME/.feynman/agent/skills}"
INDEX_JSON="$PARKED/INDEX.json"
INDEX_MD="$PARKED/INDEX.md"
# default target set; can be overridden via OPC_TARGETS or --target flag per command
DEFAULT_TARGETS="${OPC_TARGETS:-opencode,claude}"
DEFAULT_TARGETS="${OPC_TARGETS:-opencode,claude,feynman}"
# resolve a target name to its active dir
target_dir() {
case "$1" in
opencode) printf '%s\n' "$ACTIVE_OPENCODE" ;;
claude) printf '%s\n' "$ACTIVE_CLAUDE" ;;
*) echo "unknown target: $1 (valid: opencode, claude)" >&2; return 1 ;;
feynman) printf '%s\n' "$ACTIVE_FEYNMAN" ;;
*) echo "unknown target: $1 (valid: opencode, claude, feynman)" >&2; return 1 ;;
esac
}
# expand "both"/"all"/"opencode,claude" → ordered unique list of valid targets
# expand "both"/"all"/"opencode,claude,feynman" → ordered unique list of valid targets
resolve_targets() {
local raw="${1:-$DEFAULT_TARGETS}"
case "$raw" in
both|all) raw="opencode,claude" ;;
all) raw="opencode,claude,feynman" ;;
both) raw="opencode,claude" ;;
esac
printf '%s\n' "$raw" | tr ',' '\n' | awk 'NF' | awk '!seen[$0]++'
}
@@ -70,7 +74,7 @@ active_dirs_for_target() {
require_dirs() {
[ -d "$PARKED" ] || { echo "parked dir missing: $PARKED" >&2; exit 1; }
mkdir -p "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE"
mkdir -p "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" "$ACTIVE_FEYNMAN"
}
# legacy alias — many commands still use $ACTIVE for fzf preview / category / search
@@ -92,19 +96,22 @@ cmd_status() {
echo
printf "%-10s %5s %s\n" "target" "count" "dir"
local t n
for t in opencode claude; do
for t in opencode claude feynman; do
n=$(active_dirs_for_target "$t" | wc -l)
printf "%-10s %5d %s\n" "$t" "$n" "$(target_dir "$t")"
done
# union (any target) vs both (all targets)
local in_oc in_cl both any
# union (any target) vs all-three intersection
local in_oc in_cl in_fy all3 any
in_oc=$(active_dirs_for_target opencode)
in_cl=$(active_dirs_for_target claude)
both=$(comm -12 <(echo "$in_oc" | sort) <(echo "$in_cl" | sort) | grep -c .)
any=$( { echo "$in_oc"; echo "$in_cl"; } | sort -u | grep -c .)
in_fy=$(active_dirs_for_target feynman)
all3=$(comm -12 \
<(comm -12 <(echo "$in_oc" | sort) <(echo "$in_cl" | sort)) \
<(echo "$in_fy" | sort) | grep -c . || true)
any=$( { echo "$in_oc"; echo "$in_cl"; echo "$in_fy"; } | sort -u | grep -c . || true)
echo
printf "any target : %5d\n" "$any"
printf "both : %5d\n" "$both"
printf "all three : %5d\n" "$all3"
}
cmd_list() {
@@ -301,7 +308,7 @@ update_index_active_set() {
# sync shared refs (e.g. _platform-mapping.md) into all configured target dirs
sync_shared_refs_to_targets() {
local f t base
for t in opencode claude; do
for t in opencode claude feynman; do
base=$(target_dir "$t")
for f in _platform-mapping.md; do
if [ -f "$PARKED/$f" ] && [ ! -f "$base/$f" ]; then
@@ -467,34 +474,83 @@ cmd_disable_category() {
done <<< "$selection"
}
# interactive pick (active only): category → multi-select skills to disable
# interactive target picker — returns comma-separated targets on stdout, exit 1 on cancel.
# Default highlight = "all" (= opencode,claude,feynman). TAB to multi-select specific ones.
pick_targets() {
local action="${1:-enable}"
ensure_fzf
local sel
# Show counts so user can see current state per target
local oc cl fy
oc=$(active_dirs_for_target opencode | wc -l)
cl=$(active_dirs_for_target claude | wc -l)
fy=$(active_dirs_for_target feynman | wc -l)
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
}
# interactive disable-pick: union of actives across all targets → multi-select → pick targets to disable from
cmd_disable_pick() {
require_dirs
ensure_fzf
local active_count
active_count=$(skill_dirs_in "$ACTIVE" | wc -l)
[ "$active_count" -gt 0 ] || { echo "no active skills"; exit 0; }
# Active categories (by prefix)
local cats
cats=$(skill_dirs_in "$ACTIVE" | awk -F'-' '{print $1}' | sort | uniq -c | sort -rn)
local category
category=$(printf '%s\n' "$cats" | fzf --prompt="active-category > " --height=50% \
--header="Pick a category of ACTIVE skills to disable" \
| awk '{print $2}')
[ -n "$category" ] || { echo "cancelled"; exit 0; }
local selection
selection=$(skill_dirs_in "$ACTIVE" \
| awk -v p="$category" '$0 ~ "^" p "(-|$)"' \
| fzf --multi --height=80% \
--prompt="disable $category > " \
--header="TAB: toggle | ENTER: disable selected" \
--preview="sed -n '1,40p' \"$ACTIVE/{}/SKILL.md\" 2>/dev/null || echo '(no SKILL.md)'" \
--preview-window=right:60%:wrap)
[ -n "$selection" ] || { echo "cancelled"; exit 0; }
local n
# Build "skill\t[targets]" map: which targets each skill is currently active in
local tmp
tmp=$(mktemp)
{
for t in opencode claude feynman; do
while IFS= read -r n; do
[ -z "$n" ] && continue
cmd_disable "$n" || true
printf '%s\t%s\n' "$n" "$t"
done < <(active_dirs_for_target "$t")
done
} | awk -F'\t' '
{ skills[$1] = (skills[$1] ? skills[$1]"," : "") $2 }
END { for (s in skills) printf "%s\t[%s]\n", s, skills[s] }
' | sort > "$tmp"
[ -s "$tmp" ] || { rm -f "$tmp"; echo "no active skills across any target"; exit 0; }
# Active categories from the union
local cats
cats=$(awk -F'\t' '{print $1}' "$tmp" | awk -F'-' '{print $1}' | sort | uniq -c | sort -rn)
local category
category=$(printf '%s\n' "$cats" | fzf --prompt="active-category > " --height=50% \
--header="Pick category from ACTIVE skills (any target)" \
| awk '{print $2}')
[ -n "$category" ] || { rm -f "$tmp"; echo "cancelled"; exit 0; }
# Show "skill [targets]" lines, multi-select
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="sed -n '1,40p' \"$PARKED\"/{1}/SKILL.md 2>/dev/null || echo '(not parked — read from active fallback)'" \
--preview-window=right:60%:wrap)
rm -f "$tmp"
[ -n "$selection" ] || { echo "cancelled"; exit 0; }
# Pick target(s) to disable from
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"
}
@@ -516,14 +572,14 @@ cmd_disable_search() {
--delimiter='\t' --with-nth=1,2 \
--prompt="disable-search > " \
--header="TAB: toggle | ENTER: disable selected" \
--preview="sed -n '1,40p' \"$ACTIVE/{1}/SKILL.md\" 2>/dev/null || echo '(no SKILL.md)'" \
--preview="sed -n '1,40p' \"$ACTIVE\"/{1}/SKILL.md 2>/dev/null || echo '(no SKILL.md)'" \
--preview-window=right:60%:wrap < "$tmp")
else
selection=$(fzf --multi --height=80% \
--delimiter='\t' --with-nth=1,2 \
--prompt="disable-search > " \
--header="TAB: toggle | ENTER: disable selected" \
--preview="sed -n '1,40p' \"$ACTIVE/{1}/SKILL.md\" 2>/dev/null || echo '(no SKILL.md)'" \
--preview="sed -n '1,40p' \"$ACTIVE\"/{1}/SKILL.md 2>/dev/null || echo '(no SKILL.md)'" \
--preview-window=right:60%:wrap < "$tmp")
fi
rm -f "$tmp"
@@ -557,7 +613,7 @@ cmd_enable_category() {
done <<< "$selection"
}
# interactive pick: category → multi-select skills
# interactive pick: category → multi-select skills → pick target(s)
cmd_pick() {
require_dirs
ensure_fzf
@@ -572,14 +628,19 @@ cmd_pick() {
| awk -v p="$category" '$0 ~ "^" p "(-|$)"' \
| fzf --multi --height=80% \
--prompt="$category > " \
--header="TAB: toggle | ENTER: enable selected" \
--preview="sed -n '1,40p' \"$PARKED/{}/SKILL.md\" 2>/dev/null || echo '(no SKILL.md)'" \
--header="TAB: toggle | ENTER: confirm → pick target" \
--preview="sed -n '1,40p' \"$PARKED\"/{}/SKILL.md 2>/dev/null || echo '(no SKILL.md)'" \
--preview-window=right:60%:wrap)
[ -n "$selection" ] || { echo "cancelled"; exit 0; }
# Pick target(s) for enable destination
local targets
targets=$(pick_targets "enable") || { echo "cancelled (no target)"; exit 0; }
local n
while IFS= read -r n; do
[ -z "$n" ] && continue
cmd_enable "$n" || true
cmd_enable --target "$targets" "$n" || true
done <<< "$selection"
}
@@ -600,14 +661,14 @@ cmd_search() {
--delimiter='\t' --with-nth=1,2 \
--prompt="search > " \
--header="TAB: toggle | ENTER: enable selected" \
--preview="sed -n '1,40p' \"$PARKED/{1}/SKILL.md\" 2>/dev/null || echo '(not parked — maybe already active)'" \
--preview="sed -n '1,40p' \"$PARKED\"/{1}/SKILL.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 \
--prompt="search > " \
--header="TAB: toggle | ENTER: enable selected" \
--preview="sed -n '1,40p' \"$PARKED/{1}/SKILL.md\" 2>/dev/null || echo '(not parked — maybe already active)'" \
--preview="sed -n '1,40p' \"$PARKED\"/{1}/SKILL.md 2>/dev/null || echo '(not parked — maybe already active)'" \
--preview-window=right:60%:wrap < "$tmp")
fi
rm -f "$tmp"
@@ -622,7 +683,7 @@ cmd_search() {
# reindex: scan parked + per-target active dirs, regenerate INDEX.json / INDEX.md
cmd_reindex() {
require_dirs
python3 - "$PARKED" "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" "$INDEX_JSON" "$INDEX_MD" <<'PY'
python3 - "$PARKED" "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" "$ACTIVE_FEYNMAN" "$INDEX_JSON" "$INDEX_MD" <<'PY'
import sys, os, re, json
from pathlib import Path
@@ -635,8 +696,9 @@ except Exception:
parked = Path(sys.argv[1])
active_opencode = Path(sys.argv[2])
active_claude = Path(sys.argv[3])
out_json = Path(sys.argv[4])
out_md = Path(sys.argv[5])
active_feynman = Path(sys.argv[4])
out_json = Path(sys.argv[5])
out_md = Path(sys.argv[6])
def parse_frontmatter(fm: str) -> dict:
if HAVE_YAML:
@@ -709,6 +771,7 @@ def scan_target(base: Path, target_name: str):
scan_parked(parked)
scan_target(active_opencode, "opencode")
scan_target(active_claude, "claude")
scan_target(active_feynman, "feynman")
items = sorted(seen.values(), key=lambda x: x["folder"])
out_json.write_text(json.dumps(items, ensure_ascii=False, indent=2))
@@ -716,7 +779,8 @@ out_json.write_text(json.dumps(items, ensure_ascii=False, indent=2))
parked_n = sum(1 for i in items if not i["active"])
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_both = sum(1 for i in items if "opencode" in i["active"] and "claude" in i["active"])
in_fy = sum(1 for i in items if "feynman" in i["active"])
in_all3 = sum(1 for i in items if {"opencode","claude","feynman"}.issubset(set(i["active"])))
in_any = sum(1 for i in items if i["active"])
from collections import Counter
@@ -728,10 +792,10 @@ top_subs = ", ".join(f"{k}:{v}" for k,v in sub_c.most_common(10)) or "(none)"
top_tags = ", ".join(f"{k}:{v}" for k,v in tag_c.most_common(15)) or "(none)"
lines = [
"# Opencode + Claude Skills — Index",
"# Opencode + Claude + Feynman Skills — Index",
"",
f"**{len(items)} unique skills** — parked: {parked_n}, active any: {in_any}, both targets: {in_both}.",
f"Per-target — opencode: {in_oc}, claude: {in_cl}.",
f"**{len(items)} unique skills** — parked: {parked_n}, active any: {in_any}, all three: {in_all3}.",
f"Per-target — opencode: {in_oc}, claude: {in_cl}, feynman: {in_fy}.",
f"Top domains — {top_doms}.",
f"Top subdomains — {top_subs}.",
f"Top tags — {top_tags}.",
@@ -739,6 +803,7 @@ lines = [
f"Parked: `{parked}`",
f"opencode active: `{active_opencode}`",
f"claude active: `{active_claude}`",
f"feynman active: `{active_feynman}`",
"",
"See `README.md` for the `opc-skills` CLI.",
"",
@@ -755,16 +820,16 @@ for i, it in enumerate(items, 1):
f"{it.get('subdomain','') or '-'} | `{it['folder']}` | {d} | {tags_str} |"
)
out_md.write_text("\n".join(lines) + "\n")
print(f"reindexed: {len(items)} skills (parked={parked_n}, opencode={in_oc}, claude={in_cl}, both={in_both})")
print(f"reindexed: {len(items)} skills (parked={parked_n}, opencode={in_oc}, claude={in_cl}, feynman={in_fy}, all3={in_all3})")
PY
}
usage() {
cat <<'USG'
opc-skills — multi-target skill manager (opencode + claude)
opc-skills — multi-target skill manager (opencode + claude + feynman)
Targets: opencode, claude (or "both"/"all"; default = opencode,claude)
Use --target X / --target=X / -t X with enable/disable/enable-all/disable-all to scope ops.
Targets: opencode, claude, feynman ("all"=all three, "both"=opencode+claude legacy)
Default = opencode,claude,feynman. Override per-call with --target X / -t X.
Inspection:
status per-target counts + parked
@@ -781,7 +846,7 @@ Single skill:
(idempotent — skips already-active; useful as restore-from-archive)
disable-all [--target T] [-y|--yes] disable every active skill across targets
Bulk by axis (default: opencode+claude):
Bulk by axis (default: opencode+claude+feynman):
enable-category <prefix> fzf multi-pick within a verb-prefix, then enable
disable-category <prefix> fzf multi-pick of ACTIVE skills with prefix, then disable
enable-domain <domain> bulk enable parked skills by frontmatter `domain:` (eg. cybersecurity)
@@ -809,8 +874,9 @@ Environment:
OPC_PARKED override parked dir (default: ~/Documents/personas/skills-archive)
OPC_ACTIVE opencode active dir (default: ~/.config/opencode/skills)
OPC_CLAUDE_ACTIVE claude active dir (default: ~/.claude/skills)
OPC_FEYNMAN_ACTIVE feynman active dir (default: ~/.feynman/agent/skills)
OPC_TARGETS comma-separated default targets when --target omitted
(default: opencode,claude)
(default: opencode,claude,feynman)
USG
}