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:
48
README.md
48
README.md
@@ -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
|
||||
|
||||
|
||||
172
bin/opc-skills
172
bin/opc-skills
@@ -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)
|
||||
# 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
|
||||
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=$(skill_dirs_in "$ACTIVE" | awk -F'-' '{print $1}' | sort | uniq -c | sort -rn)
|
||||
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 a category of ACTIVE skills to disable" \
|
||||
--header="Pick category from ACTIVE skills (any target)" \
|
||||
| awk '{print $2}')
|
||||
[ -n "$category" ] || { echo "cancelled"; exit 0; }
|
||||
[ -n "$category" ] || { rm -f "$tmp"; echo "cancelled"; exit 0; }
|
||||
|
||||
# Show "skill [targets]" lines, multi-select
|
||||
local selection
|
||||
selection=$(skill_dirs_in "$ACTIVE" \
|
||||
| awk -v p="$category" '$0 ~ "^" p "(-|$)"' \
|
||||
selection=$(awk -F'\t' -v p="$category" '$1 ~ "^"p"(-|$)"' "$tmp" \
|
||||
| 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)'" \
|
||||
--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= read -r n; do
|
||||
while IFS=$'\t' read -r n _; do
|
||||
[ -z "$n" ] && continue
|
||||
cmd_disable "$n" || true
|
||||
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)
|
||||
@@ -806,11 +871,12 @@ Notes:
|
||||
- Single source-of-truth lives in personas repo (~/Documents/personas/skills-archive).
|
||||
|
||||
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_TARGETS comma-separated default targets when --target omitted
|
||||
(default: opencode,claude)
|
||||
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,feynman)
|
||||
USG
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user