Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Every directory that contains tools or dossiers also contains an `images/` subdirectory for their paired images.
- Every `.md` file pairs with `images/<name>.png` in the same directory group. The image filename always matches the `.md` filename (minus extension).
- Directory-style tools (e.g., `voice/voice.md`) key on the parent directory name: `voice/` pairs with `images/voice.png`. Sub-tools inside a directory may have their own images in the same `images/`.
- Skills are directories holding a `SKILL.md` plus the scripts it calls. A skill is registered by adding its directory to the `SKILLS` array in `install.sh`, and installs to `~/.claude/skills/` and `~/.cursor/skills/` rather than `~/.claude/commands/`. Unlike a command, the whole directory ships, so anything the prompt calls must live inside it.

## Image invariant

Expand Down
23 changes: 21 additions & 2 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

The installer registers the tools in this repo as user-level Claude Code slash commands by writing them into `~/.claude/commands/`. After install, each tool is invoked as `/<name>` from any Claude Code session.

It also installs **skills**, the directory-based tools that ship a script alongside the prompt. Those go to `~/.claude/skills/` and `~/.cursor/skills/`, since Claude Code and Cursor both read the `SKILL.md` format. They are invoked as `/<name>` in either agent.

## Install

```bash
curl -fsSL https://raw.githubusercontent.com/cppalliance/tools-public/master/install.sh | bash
```

Drops 37 commands into `~/.claude/commands/`. Re-run anytime to update — existing files are overwritten with the latest version. **Restart Claude Code** afterwards to pick up new commands (Claude Code does not auto-reload `commands/`).
Drops the commands into `~/.claude/commands/` and the skills into `~/.claude/skills/` and `~/.cursor/skills/`. The run prints the exact list and asks before writing anything. Re-run anytime to update — existing files are overwritten with the latest version. **Restart Claude Code** afterwards to pick up new commands (Claude Code does not auto-reload `commands/`).

## Uninstall

Expand Down Expand Up @@ -64,10 +66,27 @@ Pass these as env vars before the curl pipe (or as flags to a local `bash instal
| Env var | Effect |
| --- | --- |
| `INSTALL_YES=1` | Skip the `[y/N]` confirmation |
| `DEST=/path` | Install elsewhere than `~/.claude/commands` |
| `DEST=/path` | Install commands elsewhere than `~/.claude/commands` |
| `SKILL_DEST=a:b` | Colon-separated skill install roots. Default `~/.claude/skills:~/.cursor/skills`. Set to a single path to install for one agent only |
| `LOCAL_SRC=/path` | Use a local checkout instead of downloading the tarball |
| `UNINSTALL=1` | Run install.sh in uninstall mode (same as `uninstall.sh`) |

## Skills

A command is one markdown prompt. A skill is a directory: `SKILL.md` plus whatever scripts it calls. That is the difference that gives skills their own list and their own install path.

To add one, drop the directory in the repo and list it in the `SKILLS` array in `install.sh`, by directory rather than filename:

```bash
SKILLS=(
tools-wg21/my-skill
)
```

The installer skips any entry without a `SKILL.md`, copies the whole directory to each root in `SKILL_DEST`, and clears the previous copy first so a file dropped upstream does not linger. Uninstall removes a directory only if it exists and still contains a `SKILL.md`.

Skills that shell out to a tool the user may not have (`gh`, `python3`) should say so in the `SKILL.md` and fail with a clear message rather than a stack trace.

## What's not included

The novelist toolchain (`tools/novelist/`) is intentionally excluded — it's a coupled multi-prompt + Python pipeline that expects a per-book workspace and doesn't fit a one-shot command install.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ Given a C++ proposal, produces a scored verdict on whether it embodies the langu
_[tools/wg21/papersmith.md](tools/wg21/papersmith.md)_\
Writes WG21 papers through a six-step pipeline (commission, research, skeleton, body, surface, review) and reviews any paper through a reusable Review Process: mechanical scans, citation integrity, fact check, adversarial evaluation, resolution.

**Pick a PR to Review** (skill)\
_[tools-wg21/pick-pr-review/SKILL.md](tools-wg21/pick-pr-review/SKILL.md)_\
Scans the open PRs across the wg21 repos and names the single one worth reviewing next, ranked by whether the author is waiting on you and broken on how close the PR sits to your recent work.

**Reform Reviewer**\
_[tools/wg21/reform-reviewer.md](tools/wg21/reform-reviewer.md)_\
Takes a reform document and delivers a green/red framing report with rewrites and a verdict.
Expand Down
145 changes: 141 additions & 4 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# INSTALL_YES=1 skip the [y/N] confirmation
# UNINSTALL=1 remove instead of install
# DEST=/path override ~/.claude/commands
# SKILL_DEST=a:b override the skill install roots (colon-separated)
# LOCAL_SRC=/path use a local checkout instead of downloading the tarball

set -euo pipefail
Expand All @@ -25,6 +26,12 @@ TARBALL_URL="https://github.com/${REPO}/archive/refs/heads/${BRANCH}.tar.gz"
DEST="${DEST:-${HOME}/.claude/commands}"
LOCAL_SRC="${LOCAL_SRC:-}"

# Skills install to every agent that reads the SKILL.md format, since the same
# directory works unmodified in each. Claude Code and Cursor both also read the
# other's path as a compat fallback, but writing both explicitly avoids relying
# on that.
SKILL_DEST="${SKILL_DEST:-${HOME}/.claude/skills:${HOME}/.cursor/skills}"

# Mode: parse --uninstall flag or UNINSTALL env var.
MODE="install"
for arg in "$@"; do
Expand Down Expand Up @@ -57,8 +64,27 @@ TOP_LEVEL=(

FAMILIES=(voice interview tutor)

# Skills, as paths relative to the repo root.
#
# A command above is a single prompt file copied into ~/.claude/commands. A skill
# is a whole directory: SKILL.md plus whatever scripts it calls. That distinction
# is why they need their own list and their own install path. Add a skill here by
# its directory, not by a filename.
SKILLS=(
tools-wg21/pick-pr-review
)

# Split the colon-separated SKILL_DEST into an array.
SKILL_DESTS=()
while IFS= read -r _dest; do
[[ -n "$_dest" ]] && SKILL_DESTS+=("$_dest")
done <<< "${SKILL_DEST//:/$'\n'}"

die() { echo "error: $*" >&2; exit 1; }

# "1 skill" / "2 skills"
plural() { (( $1 == 1 )) && echo "$1 $2" || echo "$1 ${2}s"; }

extract_description() {
local file="$1"
local desc=""
Expand Down Expand Up @@ -103,11 +129,15 @@ extract_description() {
# NAMES[i] = slash-command name (with leading /)
# SOURCES[i] = path to source .md inside the extracted tree
# TARGETS[i] = absolute path under $DEST
# SKILL_NAMES[i] / SKILL_SOURCES[i] = skill command name and its source directory
plan() {
local src="$1"
local root="$2"
NAMES=()
SOURCES=()
TARGETS=()
SKILL_NAMES=()
SKILL_SOURCES=()

for f in "${TOP_LEVEL[@]}"; do
[[ -f "$src/$f" ]] || continue
Expand All @@ -134,19 +164,52 @@ plan() {
done
fi
done

# Guarded because bash 3.2, still the system bash on macOS, treats "${ARR[@]}"
# on an empty array as an unbound variable under set -u.
if (( ${#SKILLS[@]} > 0 )); then
local skill
for skill in "${SKILLS[@]}"; do
# A directory without a SKILL.md is not a skill, skip it rather than
# installing something no agent will load.
[[ -f "$root/$skill/SKILL.md" ]] || continue
SKILL_NAMES+=("/$(basename "$skill")")
SKILL_SOURCES+=("$root/$skill")
done
fi
}

# Every install path for a skill, one per line.
skill_targets() {
local name="$1" dest_root
for dest_root in "${SKILL_DESTS[@]}"; do
echo "$dest_root/$name"
done
}

# A skill counts as present if it is installed in any of the destinations.
skill_present() {
local name="$1" target
while IFS= read -r target; do
[[ -d "$target" ]] && return 0
done < <(skill_targets "$name")
return 1
}

print_banner() {
cat <<EOF

tools-public — Claude Code slash commands
==========================================
tools-public — Claude Code slash commands and skills
====================================================

A curated set of prompt-based "tools" from github.com/cppalliance/tools-public.
Each command is a markdown prompt invoked via /<name> inside Claude Code,
covering code review, document tightening, plan refinement, persona voices,
adaptive interviews, tutorials, and more.

Skills are directory-based tools that ship scripts alongside the prompt. They
install to both Claude Code and Cursor, which share the SKILL.md format.

EOF
}

Expand All @@ -162,6 +225,11 @@ print_plan() {
for name in "${NAMES[@]}"; do
(( ${#name} > max_width )) && max_width=${#name}
done
if (( ${#SKILL_NAMES[@]} > 0 )); then
for name in "${SKILL_NAMES[@]}"; do
(( ${#name} > max_width )) && max_width=${#name}
done
fi

if [[ "$action_word" == "install" ]]; then
echo "Will install ${#NAMES[@]} commands to $DEST"
Expand All @@ -188,6 +256,36 @@ print_plan() {
done
echo

if (( ${#SKILL_NAMES[@]} > 0 )); then
local skill_present_count=0
for i in "${!SKILL_NAMES[@]}"; do
skill_present "${SKILL_NAMES[$i]#/}" && skill_present_count=$((skill_present_count + 1))
done

if [[ "$action_word" == "install" ]]; then
echo "Will install $(plural ${#SKILL_NAMES[@]} skill) to:"
else
echo "Will remove $(plural ${skill_present_count} skill) from:"
fi
local dest_root
for dest_root in "${SKILL_DESTS[@]}"; do
echo " $dest_root"
done
echo

for i in "${!SKILL_NAMES[@]}"; do
local desc marker=" "
desc="$(extract_description "${SKILL_SOURCES[$i]}/SKILL.md")"
if [[ "$action_word" == "install" ]]; then
skill_present "${SKILL_NAMES[$i]#/}" && marker="↻" || marker="+"
else
skill_present "${SKILL_NAMES[$i]#/}" && marker="-" || marker=" "
fi
printf " %s %-${max_width}s %s\n" "$marker" "${SKILL_NAMES[$i]}" "$desc"
done
echo
fi

if [[ "$action_word" == "install" ]]; then
echo "Legend: + new ↻ overwrite (update)"
else
Expand Down Expand Up @@ -219,6 +317,23 @@ do_install() {
count=$((count + 1))
done
echo "Installed $count commands to $DEST."

local skill_count=0
if (( ${#SKILL_NAMES[@]} > 0 )); then
for i in "${!SKILL_NAMES[@]}"; do
local name="${SKILL_NAMES[$i]#/}" target
while IFS= read -r target; do
mkdir -p "$(dirname "$target")"
# Clear the old copy first, so a file dropped from the skill upstream
# does not linger in an install that is otherwise up to date.
[[ -d "$target" ]] && rm -rf "$target"
cp -R "${SKILL_SOURCES[$i]}" "$target"
skill_count=$((skill_count + 1))
done < <(skill_targets "$name")
done
echo "Installed $(plural ${#SKILL_NAMES[@]} skill) to $(plural ${#SKILL_DESTS[@]} location) ($(plural $skill_count copy | sed 's/copys/copies/'))."
fi

echo "Restart Claude Code to pick them up."
}

Expand All @@ -243,11 +358,31 @@ do_uninstall() {

echo "Removed $removed commands from $DEST."
(( skipped > 0 )) && echo "Skipped $skipped (not currently installed)."

local skill_removed=0
if (( ${#SKILL_NAMES[@]} > 0 )); then
for i in "${!SKILL_NAMES[@]}"; do
local name="${SKILL_NAMES[$i]#/}" target
[[ -n "$name" ]] || continue
while IFS= read -r target; do
# Only ever remove a directory we would have written: it has to exist
# and carry the SKILL.md that made it a skill in the first place.
if [[ -d "$target" && -f "$target/SKILL.md" ]]; then
rm -rf "$target"
skill_removed=$((skill_removed + 1))
fi
done < <(skill_targets "$name")
done
echo "Removed $skill_removed skill $( (( skill_removed == 1 )) && echo copy || echo copies )."
fi

return 0
}

acquire_source() {
if [[ -n "$LOCAL_SRC" ]]; then
SRC="$LOCAL_SRC/tools"
ROOT="$LOCAL_SRC"
[[ -d "$SRC" ]] || die "LOCAL_SRC=$LOCAL_SRC has no tools/ subdirectory"
echo "Source: local checkout at $LOCAL_SRC"
return
Expand All @@ -264,6 +399,8 @@ acquire_source() {

SRC="$(find "$TMP" -maxdepth 2 -type d -name tools | head -n 1)"
[[ -n "$SRC" && -d "$SRC" ]] || die "could not locate tools/ in extracted tarball"
# Skills are listed relative to the repo root, which is tools/'s parent.
ROOT="$(dirname "$SRC")"
}

main() {
Expand All @@ -273,8 +410,8 @@ main() {

acquire_source

plan "$SRC"
[[ ${#NAMES[@]} -gt 0 ]] || die "nothing to process"
plan "$SRC" "$ROOT"
[[ $(( ${#NAMES[@]} + ${#SKILL_NAMES[@]} )) -gt 0 ]] || die "nothing to process"

echo
if [[ "$MODE" == "install" ]]; then
Expand Down
Loading