Add agency-agents directory with all agent definitions

This commit is contained in:
2026-04-20 14:00:57 +08:00
parent 5d18f9d537
commit 74e69a974d
213 changed files with 50633 additions and 1 deletions

View File

@@ -0,0 +1,638 @@
#!/usr/bin/env bash
#
# convert.sh — Convert agency agent .md files into tool-specific formats.
#
# Reads all agent files from the standard category directories and outputs
# converted files to integrations/<tool>/. Run this to regenerate all
# integration files after adding or modifying agents.
#
# Usage:
# ./scripts/convert.sh [--tool <name>] [--out <dir>] [--parallel] [--jobs N] [--help]
#
# Tools:
# antigravity — Antigravity skill files (~/.gemini/antigravity/skills/)
# gemini-cli — Gemini CLI extension (skills/ + gemini-extension.json)
# opencode — OpenCode agent files (.opencode/agent/*.md)
# cursor — Cursor rule files (.cursor/rules/*.mdc)
# aider — Single CONVENTIONS.md for Aider
# windsurf — Single .windsurfrules for Windsurf
# openclaw — OpenClaw SOUL.md files (openclaw_workspace/<agent>/SOUL.md)
# qwen — Qwen Code SubAgent files (~/.qwen/agents/*.md)
# kimi — Kimi Code CLI agent files (~/.config/kimi/agents/)
# all — All tools (default)
#
# Output is written to integrations/<tool>/ relative to the repo root.
# This script never touches user config dirs — see install.sh for that.
#
# --parallel When tool is 'all', run independent tools in parallel (output order may vary).
# --jobs N Max parallel jobs when using --parallel (default: nproc or 4).
set -euo pipefail
# --- Colour helpers ---
if [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; then
GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; RED=$'\033[0;31m'; BOLD=$'\033[1m'; RESET=$'\033[0m'
else
GREEN=''; YELLOW=''; RED=''; BOLD=''; RESET=''
fi
info() { printf "${GREEN}[OK]${RESET} %s\n" "$*"; }
warn() { printf "${YELLOW}[!!]${RESET} %s\n" "$*"; }
error() { printf "${RED}[ERR]${RESET} %s\n" "$*" >&2; }
header() { echo -e "\n${BOLD}$*${RESET}"; }
# Progress bar: [=======> ] 3/8 (tqdm-style)
progress_bar() {
local current="$1" total="$2" width="${3:-20}" i filled empty
(( total > 0 )) || return
filled=$(( width * current / total ))
empty=$(( width - filled ))
printf "\r ["
for (( i=0; i<filled; i++ )); do printf "="; done
if (( filled < width )); then printf ">"; (( empty-- )); fi
for (( i=0; i<empty; i++ )); do printf " "; done
printf "] %s/%s" "$current" "$total"
[[ -t 1 ]] || printf "\n"
}
# --- Paths ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUT_DIR="$REPO_ROOT/integrations"
TODAY="$(date +%Y-%m-%d)"
AGENT_DIRS=(
academic design engineering game-development marketing paid-media sales product project-management
testing support spatial-computing specialized
)
# --- Usage ---
usage() {
sed -n '3,26p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
# Default parallel job count (nproc on Linux; sysctl on macOS when nproc missing)
parallel_jobs_default() {
local n
n=$(nproc 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
n=$(sysctl -n hw.ncpu 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
echo 4
}
# --- Frontmatter helpers ---
# Extract a single field value from YAML frontmatter block.
# Usage: get_field <field> <file>
get_field() {
local field="$1" file="$2"
awk -v f="$field" '
/^---$/ { fm++; next }
fm == 1 && $0 ~ "^" f ": " { sub("^" f ": ", ""); print; exit }
' "$file"
}
# Strip the leading frontmatter block and return only the body.
# Usage: get_body <file>
get_body() {
awk 'BEGIN{fm=0} /^---$/{fm++; next} fm>=2{print}' "$1"
}
# Convert a human-readable agent name to a lowercase kebab-case slug.
# "Frontend Developer" → "frontend-developer"
slugify() {
echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//'
}
# --- Per-tool converters ---
convert_antigravity() {
local file="$1"
local name description slug outdir outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="agency-$(slugify "$name")"
body="$(get_body "$file")"
outdir="$OUT_DIR/antigravity/$slug"
outfile="$outdir/SKILL.md"
mkdir -p "$outdir"
# Antigravity SKILL.md format mirrors community skills in ~/.gemini/antigravity/skills/
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
risk: low
source: community
date_added: '${TODAY}'
---
${body}
HEREDOC
}
convert_gemini_cli() {
local file="$1"
local name description slug outdir outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outdir="$OUT_DIR/gemini-cli/skills/$slug"
outfile="$outdir/SKILL.md"
mkdir -p "$outdir"
# Gemini CLI skill format: minimal frontmatter (name + description only)
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
---
${body}
HEREDOC
}
# Map known color names and normalize to OpenCode-safe #RRGGBB values.
resolve_opencode_color() {
local c="$1"
local mapped
c="$(printf '%s' "$c" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]')"
case "$c" in
cyan) mapped="#00FFFF" ;;
blue) mapped="#3498DB" ;;
green) mapped="#2ECC71" ;;
red) mapped="#E74C3C" ;;
purple) mapped="#9B59B6" ;;
orange) mapped="#F39C12" ;;
teal) mapped="#008080" ;;
indigo) mapped="#6366F1" ;;
pink) mapped="#E84393" ;;
gold) mapped="#EAB308" ;;
amber) mapped="#F59E0B" ;;
neon-green) mapped="#10B981" ;;
neon-cyan) mapped="#06B6D4" ;;
metallic-blue) mapped="#3B82F6" ;;
yellow) mapped="#EAB308" ;;
violet) mapped="#8B5CF6" ;;
rose) mapped="#F43F5E" ;;
lime) mapped="#84CC16" ;;
gray) mapped="#6B7280" ;;
fuchsia) mapped="#D946EF" ;;
*) mapped="$c" ;;
esac
if [[ "$mapped" =~ ^#[0-9a-fA-F]{6}$ ]]; then
printf '#%s\n' "$(printf '%s' "${mapped#\#}" | tr '[:lower:]' '[:upper:]')"
return
fi
if [[ "$mapped" =~ ^[0-9a-fA-F]{6}$ ]]; then
printf '#%s\n' "$(printf '%s' "$mapped" | tr '[:lower:]' '[:upper:]')"
return
fi
printf '#6B7280\n'
}
convert_opencode() {
local file="$1"
local name description color slug outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
color="$(resolve_opencode_color "$(get_field "color" "$file")")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outfile="$OUT_DIR/opencode/agents/${slug}.md"
mkdir -p "$OUT_DIR/opencode/agents"
# OpenCode agent format: .md with YAML frontmatter in .opencode/agents/.
# Named colors are resolved to hex via resolve_opencode_color().
cat > "$outfile" <<HEREDOC
---
name: ${name}
description: ${description}
mode: subagent
color: '${color}'
---
${body}
HEREDOC
}
convert_cursor() {
local file="$1"
local name description slug outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outfile="$OUT_DIR/cursor/rules/${slug}.mdc"
mkdir -p "$OUT_DIR/cursor/rules"
# Cursor .mdc format: description + globs + alwaysApply frontmatter
cat > "$outfile" <<HEREDOC
---
description: ${description}
globs: ""
alwaysApply: false
---
${body}
HEREDOC
}
convert_openclaw() {
local file="$1"
local name description slug outdir body
local soul_content="" agents_content=""
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outdir="$OUT_DIR/openclaw/$slug"
mkdir -p "$outdir"
# Split body sections into SOUL.md (persona) vs AGENTS.md (operations)
# by matching ## header keywords. Unmatched sections go to AGENTS.md.
#
# SOUL keywords: identity, memory (paired with identity), communication,
# style, critical rules, rules you must follow
# AGENTS keywords: everything else (mission, deliverables, workflow, etc.)
local current_target="agents" # default bucket
local current_section=""
while IFS= read -r line; do
# Detect ## headers (with or without emoji prefixes)
if [[ "$line" =~ ^##[[:space:]] ]]; then
# Flush previous section
if [[ -n "$current_section" ]]; then
if [[ "$current_target" == "soul" ]]; then
soul_content+="$current_section"
else
agents_content+="$current_section"
fi
fi
current_section=""
# Classify this header by keyword (case-insensitive)
local header_lower
header_lower="$(echo "$line" | tr '[:upper:]' '[:lower:]')"
if [[ "$header_lower" =~ identity ]] ||
[[ "$header_lower" =~ communication ]] ||
[[ "$header_lower" =~ style ]] ||
[[ "$header_lower" =~ critical.rule ]] ||
[[ "$header_lower" =~ rules.you.must.follow ]]; then
current_target="soul"
else
current_target="agents"
fi
fi
current_section+="$line"$'\n'
done <<< "$body"
# Flush final section
if [[ -n "$current_section" ]]; then
if [[ "$current_target" == "soul" ]]; then
soul_content+="$current_section"
else
agents_content+="$current_section"
fi
fi
# Write SOUL.md — persona, tone, boundaries
cat > "$outdir/SOUL.md" <<HEREDOC
${soul_content}
HEREDOC
# Write AGENTS.md — mission, deliverables, workflow
cat > "$outdir/AGENTS.md" <<HEREDOC
${agents_content}
HEREDOC
# Write IDENTITY.md — emoji + name + vibe from frontmatter, fallback to description
local emoji vibe
emoji="$(get_field "emoji" "$file")"
vibe="$(get_field "vibe" "$file")"
if [[ -n "$emoji" && -n "$vibe" ]]; then
cat > "$outdir/IDENTITY.md" <<HEREDOC
# ${emoji} ${name}
${vibe}
HEREDOC
else
cat > "$outdir/IDENTITY.md" <<HEREDOC
# ${name}
${description}
HEREDOC
fi
}
convert_qwen() {
local file="$1"
local name description tools slug outfile body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
tools="$(get_field "tools" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outfile="$OUT_DIR/qwen/agents/${slug}.md"
mkdir -p "$(dirname "$outfile")"
# Qwen Code SubAgent format: .md with YAML frontmatter in ~/.qwen/agents/
# name and description required; tools optional (only if present in source)
if [[ -n "$tools" ]]; then
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
tools: ${tools}
---
${body}
HEREDOC
else
cat > "$outfile" <<HEREDOC
---
name: ${slug}
description: ${description}
---
${body}
HEREDOC
fi
}
convert_kimi() {
local file="$1"
local name description slug outdir agent_file body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
slug="$(slugify "$name")"
body="$(get_body "$file")"
outdir="$OUT_DIR/kimi/$slug"
agent_file="$outdir/agent.yaml"
mkdir -p "$outdir"
# Kimi Code CLI agent format: YAML with separate system prompt file
# Uses extend: default to inherit Kimi's default toolset
cat > "$agent_file" <<HEREDOC
version: 1
agent:
name: ${slug}
extend: default
system_prompt_path: ./system.md
HEREDOC
# Write system prompt to separate file
cat > "$outdir/system.md" <<HEREDOC
# ${name}
${description}
${body}
HEREDOC
}
# Aider and Windsurf are single-file formats — accumulate into temp files
# then write at the end.
AIDER_TMP="$(mktemp)"
WINDSURF_TMP="$(mktemp)"
trap 'rm -f "$AIDER_TMP" "$WINDSURF_TMP"' EXIT
# Write Aider/Windsurf headers once
cat > "$AIDER_TMP" <<'HEREDOC'
# The Agency — AI Agent Conventions
#
# This file provides Aider with the full roster of specialized AI agents from
# The Agency (https://github.com/msitarzewski/agency-agents).
#
# To activate an agent, reference it by name in your Aider session prompt, e.g.:
# "Use the Frontend Developer agent to review this component."
#
# Generated by scripts/convert.sh — do not edit manually.
HEREDOC
cat > "$WINDSURF_TMP" <<'HEREDOC'
# The Agency — AI Agent Rules for Windsurf
#
# Full roster of specialized AI agents from The Agency.
# To activate an agent, reference it by name in your Windsurf conversation.
#
# Generated by scripts/convert.sh — do not edit manually.
HEREDOC
accumulate_aider() {
local file="$1"
local name description body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
body="$(get_body "$file")"
cat >> "$AIDER_TMP" <<HEREDOC
---
## ${name}
> ${description}
${body}
HEREDOC
}
accumulate_windsurf() {
local file="$1"
local name description body
name="$(get_field "name" "$file")"
description="$(get_field "description" "$file")"
body="$(get_body "$file")"
cat >> "$WINDSURF_TMP" <<HEREDOC
================================================================================
## ${name}
${description}
================================================================================
${body}
HEREDOC
}
# --- Main loop ---
run_conversions() {
local tool="$1"
local count=0
for dir in "${AGENT_DIRS[@]}"; do
local dirpath="$REPO_ROOT/$dir"
[[ -d "$dirpath" ]] || continue
while IFS= read -r -d '' file; do
# Skip files without frontmatter (non-agent docs like QUICKSTART.md)
local first_line
first_line="$(head -1 "$file")"
[[ "$first_line" == "---" ]] || continue
local name
name="$(get_field "name" "$file")"
[[ -n "$name" ]] || continue
case "$tool" in
antigravity) convert_antigravity "$file" ;;
gemini-cli) convert_gemini_cli "$file" ;;
opencode) convert_opencode "$file" ;;
cursor) convert_cursor "$file" ;;
openclaw) convert_openclaw "$file" ;;
qwen) convert_qwen "$file" ;;
kimi) convert_kimi "$file" ;;
aider) accumulate_aider "$file" ;;
windsurf) accumulate_windsurf "$file" ;;
esac
(( count++ )) || true
done < <(find "$dirpath" -name "*.md" -type f -print0 | sort -z)
done
echo "$count"
}
# --- Entry point ---
main() {
local tool="all"
local use_parallel=false
local parallel_jobs
parallel_jobs="$(parallel_jobs_default)"
while [[ $# -gt 0 ]]; do
case "$1" in
--tool) tool="${2:?'--tool requires a value'}"; shift 2 ;;
--out) OUT_DIR="${2:?'--out requires a value'}"; shift 2 ;;
--parallel) use_parallel=true; shift ;;
--jobs) parallel_jobs="${2:?'--jobs requires a value'}"; shift 2 ;;
--help|-h) usage ;;
*) error "Unknown option: $1"; usage ;;
esac
done
local valid_tools=("antigravity" "gemini-cli" "opencode" "cursor" "aider" "windsurf" "openclaw" "qwen" "kimi" "all")
local valid=false
for t in "${valid_tools[@]}"; do [[ "$t" == "$tool" ]] && valid=true && break; done
if ! $valid; then
error "Unknown tool '$tool'. Valid: ${valid_tools[*]}"
exit 1
fi
header "The Agency -- Converting agents to tool-specific formats"
echo " Repo: $REPO_ROOT"
echo " Output: $OUT_DIR"
echo " Tool: $tool"
echo " Date: $TODAY"
if $use_parallel && [[ "$tool" == "all" ]]; then
info "Parallel mode: output buffered so each tool's output stays together."
fi
local tools_to_run=()
if [[ "$tool" == "all" ]]; then
tools_to_run=("antigravity" "gemini-cli" "opencode" "cursor" "aider" "windsurf" "openclaw" "qwen" "kimi")
else
tools_to_run=("$tool")
fi
local total=0
local n_tools=${#tools_to_run[@]}
if $use_parallel && [[ "$tool" == "all" ]]; then
# Tools that write to separate dirs can run in parallel; buffer output so each tool's output stays together
local parallel_tools=(antigravity gemini-cli opencode cursor openclaw qwen)
local parallel_out_dir
parallel_out_dir="$(mktemp -d)"
info "Converting: ${#parallel_tools[@]}/${n_tools} tools in parallel (output buffered per tool)..."
export AGENCY_CONVERT_OUT_DIR="$parallel_out_dir"
export AGENCY_CONVERT_SCRIPT="$SCRIPT_DIR/convert.sh"
export AGENCY_CONVERT_OUT="$OUT_DIR"
printf '%s\n' "${parallel_tools[@]}" | xargs -P "$parallel_jobs" -I {} sh -c '"$AGENCY_CONVERT_SCRIPT" --tool "{}" --out "$AGENCY_CONVERT_OUT" > "$AGENCY_CONVERT_OUT_DIR/{}" 2>&1'
for t in "${parallel_tools[@]}"; do
[[ -f "$parallel_out_dir/$t" ]] && cat "$parallel_out_dir/$t"
done
rm -rf "$parallel_out_dir"
local idx=7
for t in aider windsurf; do
progress_bar "$idx" "$n_tools"
printf "\n"
header "Converting: $t ($idx/$n_tools)"
local count
count="$(run_conversions "$t")"
total=$(( total + count ))
info "Converted $count agents for $t"
(( idx++ )) || true
done
else
local i=0
for t in "${tools_to_run[@]}"; do
(( i++ )) || true
progress_bar "$i" "$n_tools"
printf "\n"
header "Converting: $t ($i/$n_tools)"
local count
count="$(run_conversions "$t")"
total=$(( total + count ))
# Gemini CLI also needs the extension manifest (written by this process when --tool gemini-cli)
if [[ "$t" == "gemini-cli" ]]; then
mkdir -p "$OUT_DIR/gemini-cli"
cat > "$OUT_DIR/gemini-cli/gemini-extension.json" <<'HEREDOC'
{
"name": "agency-agents",
"version": "1.0.0"
}
HEREDOC
info "Wrote gemini-extension.json"
fi
info "Converted $count agents for $t"
done
fi
# Write single-file outputs after accumulation
if [[ "$tool" == "all" || "$tool" == "aider" ]]; then
mkdir -p "$OUT_DIR/aider"
cp "$AIDER_TMP" "$OUT_DIR/aider/CONVENTIONS.md"
info "Wrote integrations/aider/CONVENTIONS.md"
fi
if [[ "$tool" == "all" || "$tool" == "windsurf" ]]; then
mkdir -p "$OUT_DIR/windsurf"
cp "$WINDSURF_TMP" "$OUT_DIR/windsurf/.windsurfrules"
info "Wrote integrations/windsurf/.windsurfrules"
fi
echo ""
if $use_parallel && [[ "$tool" == "all" ]]; then
info "Done. $n_tools tools (parallel; total conversions not aggregated)."
else
info "Done. Total conversions: $total"
fi
}
main "$@"

View File

@@ -0,0 +1,639 @@
#!/usr/bin/env bash
#
# install.sh -- Install The Agency agents into your local agentic tool(s).
#
# Reads converted files from integrations/ and copies them to the appropriate
# config directory for each tool. Run scripts/convert.sh first if integrations/
# is missing or stale.
#
# Usage:
# ./scripts/install.sh [--tool <name>] [--interactive] [--no-interactive] [--parallel] [--jobs N] [--help]
#
# Tools:
# claude-code -- Copy agents to ~/.claude/agents/
# copilot -- Copy agents to ~/.github/agents/ and ~/.copilot/agents/
# antigravity -- Copy skills to ~/.gemini/antigravity/skills/
# gemini-cli -- Install extension to ~/.gemini/extensions/agency-agents/
# opencode -- Copy agents to .opencode/agent/ in current directory
# cursor -- Copy rules to .cursor/rules/ in current directory
# aider -- Copy CONVENTIONS.md to current directory
# windsurf -- Copy .windsurfrules to current directory
# openclaw -- Copy workspaces to ~/.openclaw/agency-agents/
# qwen -- Copy SubAgents to ~/.qwen/agents/ (user-wide) or .qwen/agents/ (project)
# all -- Install for all detected tools (default)
#
# Flags:
# --tool <name> Install only the specified tool
# --interactive Show interactive selector (default when run in a terminal)
# --no-interactive Skip interactive selector, install all detected tools
# --parallel Run install for each selected tool in parallel (output order may vary)
# --jobs N Max parallel jobs when using --parallel (default: nproc or 4)
# --help Show this help
#
# Platform support:
# Linux, macOS (requires bash 3.2+), Windows Git Bash / WSL
set -euo pipefail
# ---------------------------------------------------------------------------
# Colours -- only when stdout supports color
# ---------------------------------------------------------------------------
if [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; then
C_GREEN=$'\033[0;32m'
C_YELLOW=$'\033[1;33m'
C_RED=$'\033[0;31m'
C_CYAN=$'\033[0;36m'
C_BOLD=$'\033[1m'
C_DIM=$'\033[2m'
C_RESET=$'\033[0m'
else
C_GREEN=''; C_YELLOW=''; C_RED=''; C_CYAN=''; C_BOLD=''; C_DIM=''; C_RESET=''
fi
ok() { printf "${C_GREEN}[OK]${C_RESET} %s\n" "$*"; }
warn() { printf "${C_YELLOW}[!!]${C_RESET} %s\n" "$*"; }
err() { printf "${C_RED}[ERR]${C_RESET} %s\n" "$*" >&2; }
header() { printf "\n${C_BOLD}%s${C_RESET}\n" "$*"; }
dim() { printf "${C_DIM}%s${C_RESET}\n" "$*"; }
# Progress bar: [=======> ] 3/8 (tqdm-style)
progress_bar() {
local current="$1" total="$2" width="${3:-20}" i filled empty
(( total > 0 )) || return
filled=$(( width * current / total ))
empty=$(( width - filled ))
printf "\r ["
for (( i=0; i<filled; i++ )); do printf "="; done
if (( filled < width )); then printf ">"; (( empty-- )); fi
for (( i=0; i<empty; i++ )); do printf " "; done
printf "] %s/%s" "$current" "$total"
[[ -t 1 ]] || printf "\n"
}
# ---------------------------------------------------------------------------
# Box drawing -- pure ASCII, fixed 52-char wide
# box_top / box_mid / box_bot -- structural lines
# box_row <text> -- content row, right-padded to fit
# ---------------------------------------------------------------------------
BOX_INNER=48 # chars between the two | walls
box_top() { printf " +"; printf '%0.s-' $(seq 1 $BOX_INNER); printf "+\n"; }
box_bot() { box_top; }
box_sep() { printf " |"; printf '%0.s-' $(seq 1 $BOX_INNER); printf "|\n"; }
strip_ansi() {
awk '{ gsub(/\033\[[0-9;]*m/, ""); print }' <<< "$1"
}
box_row() {
# Strip ANSI escapes when measuring visible length
local raw="$1"
local visible
visible="$(strip_ansi "$raw")"
local pad=$(( BOX_INNER - 2 - ${#visible} ))
if (( pad < 0 )); then pad=0; fi
printf " | %s%*s |\n" "$raw" "$pad" ''
}
box_blank() { printf " |%*s|\n" $BOX_INNER ''; }
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
INTEGRATIONS="$REPO_ROOT/integrations"
ALL_TOOLS=(claude-code copilot antigravity gemini-cli opencode openclaw cursor aider windsurf qwen kimi)
# ---------------------------------------------------------------------------
# Usage
# ---------------------------------------------------------------------------
usage() {
sed -n '3,32p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
# Default parallel job count (nproc on Linux; sysctl on macOS when nproc missing)
parallel_jobs_default() {
local n
n=$(nproc 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
n=$(sysctl -n hw.ncpu 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
echo 4
}
# ---------------------------------------------------------------------------
# Preflight
# ---------------------------------------------------------------------------
check_integrations() {
if [[ ! -d "$INTEGRATIONS" ]]; then
err "integrations/ not found. Run ./scripts/convert.sh first."
exit 1
fi
}
# ---------------------------------------------------------------------------
# Tool detection
# ---------------------------------------------------------------------------
detect_claude_code() { [[ -d "${HOME}/.claude" ]]; }
detect_copilot() { command -v code >/dev/null 2>&1 || [[ -d "${HOME}/.github" || -d "${HOME}/.copilot" ]]; }
detect_antigravity() { [[ -d "${HOME}/.gemini/antigravity/skills" ]]; }
detect_gemini_cli() { command -v gemini >/dev/null 2>&1 || [[ -d "${HOME}/.gemini" ]]; }
detect_cursor() { command -v cursor >/dev/null 2>&1 || [[ -d "${HOME}/.cursor" ]]; }
detect_opencode() { command -v opencode >/dev/null 2>&1 || [[ -d "${HOME}/.config/opencode" ]]; }
detect_aider() { command -v aider >/dev/null 2>&1; }
detect_openclaw() { command -v openclaw >/dev/null 2>&1 || [[ -d "${HOME}/.openclaw" ]]; }
detect_windsurf() { command -v windsurf >/dev/null 2>&1 || [[ -d "${HOME}/.codeium" ]]; }
detect_qwen() { command -v qwen >/dev/null 2>&1 || [[ -d "${HOME}/.qwen" ]]; }
detect_kimi() { command -v kimi >/dev/null 2>&1; }
is_detected() {
case "$1" in
claude-code) detect_claude_code ;;
copilot) detect_copilot ;;
antigravity) detect_antigravity ;;
gemini-cli) detect_gemini_cli ;;
opencode) detect_opencode ;;
openclaw) detect_openclaw ;;
cursor) detect_cursor ;;
aider) detect_aider ;;
windsurf) detect_windsurf ;;
qwen) detect_qwen ;;
kimi) detect_kimi ;;
*) return 1 ;;
esac
}
# Fixed-width labels: name (14) + detail (24) = 38 visible chars
tool_label() {
case "$1" in
claude-code) printf "%-14s %s" "Claude Code" "(claude.ai/code)" ;;
copilot) printf "%-14s %s" "Copilot" "(~/.github + ~/.copilot)" ;;
antigravity) printf "%-14s %s" "Antigravity" "(~/.gemini/antigravity)" ;;
gemini-cli) printf "%-14s %s" "Gemini CLI" "(gemini extension)" ;;
opencode) printf "%-14s %s" "OpenCode" "(opencode.ai)" ;;
openclaw) printf "%-14s %s" "OpenClaw" "(~/.openclaw)" ;;
cursor) printf "%-14s %s" "Cursor" "(.cursor/rules)" ;;
aider) printf "%-14s %s" "Aider" "(CONVENTIONS.md)" ;;
windsurf) printf "%-14s %s" "Windsurf" "(.windsurfrules)" ;;
qwen) printf "%-14s %s" "Qwen Code" "(~/.qwen/agents)" ;;
kimi) printf "%-14s %s" "Kimi Code" "(~/.config/kimi/agents)" ;;
esac
}
# ---------------------------------------------------------------------------
# Interactive selector
# ---------------------------------------------------------------------------
interactive_select() {
# bash 3-compatible arrays
declare -a selected=()
declare -a detected_map=()
local t
for t in "${ALL_TOOLS[@]}"; do
if is_detected "$t" 2>/dev/null; then
selected+=(1); detected_map+=(1)
else
selected+=(0); detected_map+=(0)
fi
done
while true; do
# --- header ---
printf "\n"
box_top
box_row "${C_BOLD} The Agency -- Tool Installer${C_RESET}"
box_bot
printf "\n"
printf " ${C_DIM}System scan: [*] = detected on this machine${C_RESET}\n"
printf "\n"
# --- tool rows ---
local i=0
for t in "${ALL_TOOLS[@]}"; do
local num=$(( i + 1 ))
local label
label="$(tool_label "$t")"
local dot
if [[ "${detected_map[$i]}" == "1" ]]; then
dot="${C_GREEN}[*]${C_RESET}"
else
dot="${C_DIM}[ ]${C_RESET}"
fi
local chk
if [[ "${selected[$i]}" == "1" ]]; then
chk="${C_GREEN}[x]${C_RESET}"
else
chk="${C_DIM}[ ]${C_RESET}"
fi
printf " %s %s) %s %s\n" "$chk" "$num" "$dot" "$label"
(( i++ )) || true
done
# --- controls ---
printf "\n"
printf " ------------------------------------------------\n"
printf " ${C_CYAN}[1-%s]${C_RESET} toggle ${C_CYAN}[a]${C_RESET} all ${C_CYAN}[n]${C_RESET} none ${C_CYAN}[d]${C_RESET} detected\n" "${#ALL_TOOLS[@]}"
printf " ${C_GREEN}[Enter]${C_RESET} install ${C_RED}[q]${C_RESET} quit\n"
printf "\n"
printf " >> "
read -r input </dev/tty
case "$input" in
q|Q)
printf "\n"; ok "Aborted."; exit 0 ;;
a|A)
for (( j=0; j<${#ALL_TOOLS[@]}; j++ )); do selected[$j]=1; done ;;
n|N)
for (( j=0; j<${#ALL_TOOLS[@]}; j++ )); do selected[$j]=0; done ;;
d|D)
for (( j=0; j<${#ALL_TOOLS[@]}; j++ )); do selected[$j]="${detected_map[$j]}"; done ;;
"")
local any=false
local s
for s in "${selected[@]}"; do [[ "$s" == "1" ]] && any=true && break; done
if $any; then
break
else
printf " ${C_YELLOW}Nothing selected -- pick a tool or press q to quit.${C_RESET}\n"
sleep 1
fi ;;
*)
local toggled=false
local num
for num in $input; do
if [[ "$num" =~ ^[0-9]+$ ]]; then
local idx=$(( num - 1 ))
if (( idx >= 0 && idx < ${#ALL_TOOLS[@]} )); then
if [[ "${selected[$idx]}" == "1" ]]; then
selected[$idx]=0
else
selected[$idx]=1
fi
toggled=true
fi
fi
done
if ! $toggled; then
printf " ${C_RED}Invalid. Enter a number 1-%s, or a command.${C_RESET}\n" "${#ALL_TOOLS[@]}"
sleep 1
fi ;;
esac
# Clear UI for redraw
local lines=$(( ${#ALL_TOOLS[@]} + 14 ))
local l
for (( l=0; l<lines; l++ )); do printf '\033[1A\033[2K'; done
done
# Build output array
SELECTED_TOOLS=()
local i=0
for t in "${ALL_TOOLS[@]}"; do
[[ "${selected[$i]}" == "1" ]] && SELECTED_TOOLS+=("$t")
(( i++ )) || true
done
}
# ---------------------------------------------------------------------------
# Installers
# ---------------------------------------------------------------------------
install_claude_code() {
local dest="${HOME}/.claude/agents"
local count=0
mkdir -p "$dest"
local dir f first_line
for dir in academic design engineering game-development marketing paid-media sales product project-management \
testing support spatial-computing specialized; do
[[ -d "$REPO_ROOT/$dir" ]] || continue
while IFS= read -r -d '' f; do
first_line="$(head -1 "$f")"
[[ "$first_line" == "---" ]] || continue
cp "$f" "$dest/"
(( count++ )) || true
done < <(find "$REPO_ROOT/$dir" -name "*.md" -type f -print0)
done
ok "Claude Code: $count agents -> $dest"
}
install_copilot() {
local dest_github="${HOME}/.github/agents"
local dest_copilot="${HOME}/.copilot/agents"
local count=0
mkdir -p "$dest_github" "$dest_copilot"
local dir f first_line
for dir in academic design engineering game-development marketing paid-media sales product project-management \
testing support spatial-computing specialized; do
[[ -d "$REPO_ROOT/$dir" ]] || continue
while IFS= read -r -d '' f; do
first_line="$(head -1 "$f")"
[[ "$first_line" == "---" ]] || continue
cp "$f" "$dest_github/"
cp "$f" "$dest_copilot/"
(( count++ )) || true
done < <(find "$REPO_ROOT/$dir" -name "*.md" -type f -print0)
done
ok "Copilot: $count agents -> $dest_github"
ok "Copilot: $count agents -> $dest_copilot"
}
install_antigravity() {
local src="$INTEGRATIONS/antigravity"
local dest="${HOME}/.gemini/antigravity/skills"
local count=0
[[ -d "$src" ]] || { err "integrations/antigravity missing. Run convert.sh first."; return 1; }
mkdir -p "$dest"
local d
while IFS= read -r -d '' d; do
local name; name="$(basename "$d")"
mkdir -p "$dest/$name"
cp "$d/SKILL.md" "$dest/$name/SKILL.md"
(( count++ )) || true
done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
ok "Antigravity: $count skills -> $dest"
}
install_gemini_cli() {
local src="$INTEGRATIONS/gemini-cli"
local dest="${HOME}/.gemini/extensions/agency-agents"
local count=0
local manifest="$src/gemini-extension.json"
local skills_dir="$src/skills"
[[ -d "$src" ]] || { err "integrations/gemini-cli missing. Run ./scripts/convert.sh --tool gemini-cli first."; return 1; }
[[ -f "$manifest" ]] || { err "integrations/gemini-cli/gemini-extension.json missing. Run ./scripts/convert.sh --tool gemini-cli first."; return 1; }
[[ -d "$skills_dir" ]] || { err "integrations/gemini-cli/skills missing. Run ./scripts/convert.sh --tool gemini-cli first."; return 1; }
mkdir -p "$dest/skills"
cp "$manifest" "$dest/gemini-extension.json"
local d
while IFS= read -r -d '' d; do
local name; name="$(basename "$d")"
mkdir -p "$dest/skills/$name"
cp "$d/SKILL.md" "$dest/skills/$name/SKILL.md"
(( count++ )) || true
done < <(find "$skills_dir" -mindepth 1 -maxdepth 1 -type d -print0)
ok "Gemini CLI: $count skills -> $dest"
}
install_opencode() {
local src="$INTEGRATIONS/opencode/agents"
local dest="${PWD}/.opencode/agents"
local count=0
[[ -d "$src" ]] || { err "integrations/opencode missing. Run convert.sh first."; return 1; }
mkdir -p "$dest"
local f
while IFS= read -r -d '' f; do
cp "$f" "$dest/"; (( count++ )) || true
done < <(find "$src" -maxdepth 1 -name "*.md" -print0)
ok "OpenCode: $count agents -> $dest"
warn "OpenCode: project-scoped. Run from your project root to install there."
}
install_openclaw() {
local src="$INTEGRATIONS/openclaw"
local dest="${HOME}/.openclaw/agency-agents"
local count=0
[[ -d "$src" ]] || { err "integrations/openclaw missing. Run convert.sh first."; return 1; }
mkdir -p "$dest"
local d
while IFS= read -r -d '' d; do
local name; name="$(basename "$d")"
mkdir -p "$dest/$name"
cp "$d/SOUL.md" "$dest/$name/SOUL.md"
cp "$d/AGENTS.md" "$dest/$name/AGENTS.md"
cp "$d/IDENTITY.md" "$dest/$name/IDENTITY.md"
# Register with OpenClaw so agents are usable by agentId immediately
if command -v openclaw >/dev/null 2>&1; then
openclaw agents add "$name" --workspace "$dest/$name" --non-interactive || true
fi
(( count++ )) || true
done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
ok "OpenClaw: $count workspaces -> $dest"
if command -v openclaw >/dev/null 2>&1; then
warn "OpenClaw: run 'openclaw gateway restart' to activate new agents"
fi
}
install_cursor() {
local src="$INTEGRATIONS/cursor/rules"
local dest="${PWD}/.cursor/rules"
local count=0
[[ -d "$src" ]] || { err "integrations/cursor missing. Run convert.sh first."; return 1; }
mkdir -p "$dest"
local f
while IFS= read -r -d '' f; do
cp "$f" "$dest/"; (( count++ )) || true
done < <(find "$src" -maxdepth 1 -name "*.mdc" -print0)
ok "Cursor: $count rules -> $dest"
warn "Cursor: project-scoped. Run from your project root to install there."
}
install_aider() {
local src="$INTEGRATIONS/aider/CONVENTIONS.md"
local dest="${PWD}/CONVENTIONS.md"
[[ -f "$src" ]] || { err "integrations/aider/CONVENTIONS.md missing. Run convert.sh first."; return 1; }
if [[ -f "$dest" ]]; then
warn "Aider: CONVENTIONS.md already exists at $dest (remove to reinstall)."
return 0
fi
cp "$src" "$dest"
ok "Aider: installed -> $dest"
warn "Aider: project-scoped. Run from your project root to install there."
}
install_windsurf() {
local src="$INTEGRATIONS/windsurf/.windsurfrules"
local dest="${PWD}/.windsurfrules"
[[ -f "$src" ]] || { err "integrations/windsurf/.windsurfrules missing. Run convert.sh first."; return 1; }
if [[ -f "$dest" ]]; then
warn "Windsurf: .windsurfrules already exists at $dest (remove to reinstall)."
return 0
fi
cp "$src" "$dest"
ok "Windsurf: installed -> $dest"
warn "Windsurf: project-scoped. Run from your project root to install there."
}
install_qwen() {
local src="$INTEGRATIONS/qwen/agents"
local dest="${PWD}/.qwen/agents"
local count=0
[[ -d "$src" ]] || { err "integrations/qwen missing. Run convert.sh first."; return 1; }
mkdir -p "$dest"
local f
while IFS= read -r -d '' f; do
cp "$f" "$dest/"
(( count++ )) || true
done < <(find "$src" -maxdepth 1 -name "*.md" -print0)
ok "Qwen Code: installed $count agents to $dest"
warn "Qwen Code: project-scoped. Run from your project root to install there."
warn "Tip: Run '/agents manage' in Qwen Code to refresh, or restart session"
}
install_kimi() {
local src="$INTEGRATIONS/kimi"
local dest="${HOME}/.config/kimi/agents"
local count=0
[[ -d "$src" ]] || { err "integrations/kimi missing. Run convert.sh first."; return 1; }
mkdir -p "$dest"
local d
while IFS= read -r -d '' d; do
local name; name="$(basename "$d")"
mkdir -p "$dest/$name"
cp "$d/agent.yaml" "$dest/$name/agent.yaml"
cp "$d/system.md" "$dest/$name/system.md"
(( count++ )) || true
done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
ok "Kimi Code: installed $count agents to $dest"
ok "Usage: kimi --agent-file ~/.config/kimi/agents/<agent-name>/agent.yaml"
}
install_tool() {
case "$1" in
claude-code) install_claude_code ;;
copilot) install_copilot ;;
antigravity) install_antigravity ;;
gemini-cli) install_gemini_cli ;;
opencode) install_opencode ;;
openclaw) install_openclaw ;;
cursor) install_cursor ;;
aider) install_aider ;;
windsurf) install_windsurf ;;
qwen) install_qwen ;;
kimi) install_kimi ;;
esac
}
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
main() {
local tool="all"
local interactive_mode="auto"
local use_parallel=false
local parallel_jobs
parallel_jobs="$(parallel_jobs_default)"
while [[ $# -gt 0 ]]; do
case "$1" in
--tool) tool="${2:?'--tool requires a value'}"; shift 2; interactive_mode="no" ;;
--interactive) interactive_mode="yes"; shift ;;
--no-interactive) interactive_mode="no"; shift ;;
--parallel) use_parallel=true; shift ;;
--jobs) parallel_jobs="${2:?'--jobs requires a value'}"; shift 2 ;;
--help|-h) usage ;;
*) err "Unknown option: $1"; usage ;;
esac
done
check_integrations
# Validate explicit tool
if [[ "$tool" != "all" ]]; then
local valid=false t
for t in "${ALL_TOOLS[@]}"; do [[ "$t" == "$tool" ]] && valid=true && break; done
if ! $valid; then
err "Unknown tool '$tool'. Valid: ${ALL_TOOLS[*]}"
exit 1
fi
fi
# Decide whether to show interactive UI
local use_interactive=false
if [[ "$interactive_mode" == "yes" ]]; then
use_interactive=true
elif [[ "$interactive_mode" == "auto" && -t 0 && -t 1 && "$tool" == "all" ]]; then
use_interactive=true
fi
SELECTED_TOOLS=()
if $use_interactive; then
interactive_select
elif [[ "$tool" != "all" ]]; then
SELECTED_TOOLS=("$tool")
else
# Non-interactive: auto-detect
header "The Agency -- Scanning for installed tools..."
printf "\n"
local t
for t in "${ALL_TOOLS[@]}"; do
if is_detected "$t" 2>/dev/null; then
SELECTED_TOOLS+=("$t")
printf " ${C_GREEN}[*]${C_RESET} %s ${C_DIM}detected${C_RESET}\n" "$(tool_label "$t")"
else
printf " ${C_DIM}[ ] %s not found${C_RESET}\n" "$(tool_label "$t")"
fi
done
fi
if [[ ${#SELECTED_TOOLS[@]} -eq 0 ]]; then
warn "No tools selected or detected. Nothing to install."
printf "\n"
dim " Tip: use --tool <name> to force-install a specific tool."
dim " Available: ${ALL_TOOLS[*]}"
exit 0
fi
# When parent runs install.sh --parallel, it spawns workers with AGENCY_INSTALL_WORKER=1
# so each worker only runs install_tool(s) and skips header/done box (avoids duplicate output).
if [[ -n "${AGENCY_INSTALL_WORKER:-}" ]]; then
local t
for t in "${SELECTED_TOOLS[@]}"; do
install_tool "$t"
done
return 0
fi
printf "\n"
header "The Agency -- Installing agents"
printf " Repo: %s\n" "$REPO_ROOT"
local n_selected=${#SELECTED_TOOLS[@]}
printf " Installing: %s\n" "${SELECTED_TOOLS[*]}"
if $use_parallel; then
ok "Installing $n_selected tools in parallel (output buffered per tool)."
fi
printf "\n"
local installed=0 t i=0
if $use_parallel; then
local install_out_dir
install_out_dir="$(mktemp -d)"
export AGENCY_INSTALL_OUT_DIR="$install_out_dir"
export AGENCY_INSTALL_SCRIPT="$SCRIPT_DIR/install.sh"
printf '%s\n' "${SELECTED_TOOLS[@]}" | xargs -P "$parallel_jobs" -I {} sh -c 'AGENCY_INSTALL_WORKER=1 "$AGENCY_INSTALL_SCRIPT" --tool "{}" --no-interactive > "$AGENCY_INSTALL_OUT_DIR/{}" 2>&1'
for t in "${SELECTED_TOOLS[@]}"; do
[[ -f "$install_out_dir/$t" ]] && cat "$install_out_dir/$t"
done
rm -rf "$install_out_dir"
installed=$n_selected
else
for t in "${SELECTED_TOOLS[@]}"; do
(( i++ )) || true
progress_bar "$i" "$n_selected"
printf "\n"
printf " ${C_DIM}[%s/%s]${C_RESET} %s\n" "$i" "$n_selected" "$t"
install_tool "$t"
(( installed++ )) || true
done
fi
# Done box
local msg=" Done! Installed $installed tool(s)."
printf "\n"
box_top
box_row "${C_GREEN}${C_BOLD}${msg}${C_RESET}"
box_bot
printf "\n"
dim " Run ./scripts/convert.sh to regenerate after adding or editing agents."
printf "\n"
}
main "$@"

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env bash
#
# Validates agent markdown files:
# 1. YAML frontmatter must exist with name, description, color (ERROR)
# 2. Recommended sections checked but only warned (WARN)
# 3. File must have meaningful content
#
# Usage: ./scripts/lint-agents.sh [file ...]
# If no files given, scans all agent directories.
set -euo pipefail
AGENT_DIRS=(
design
engineering
game-development
marketing
paid-media
product
project-management
testing
support
spatial-computing
specialized
)
REQUIRED_FRONTMATTER=("name" "description" "color")
RECOMMENDED_SECTIONS=("Identity" "Core Mission" "Critical Rules")
errors=0
warnings=0
lint_file() {
local file="$1"
# 1. Check frontmatter delimiters
local first_line
first_line=$(head -1 "$file")
if [[ "$first_line" != "---" ]]; then
echo "ERROR $file: missing frontmatter opening ---"
errors=$((errors + 1))
return
fi
# Extract frontmatter (between first and second ---)
local frontmatter
frontmatter=$(awk 'NR==1{next} /^---$/{exit} {print}' "$file")
if [[ -z "$frontmatter" ]]; then
echo "ERROR $file: empty or malformed frontmatter"
errors=$((errors + 1))
return
fi
# 2. Check required frontmatter fields
for field in "${REQUIRED_FRONTMATTER[@]}"; do
if ! echo "$frontmatter" | grep -qE "^${field}:"; then
echo "ERROR $file: missing frontmatter field '${field}'"
errors=$((errors + 1))
fi
done
# 3. Check recommended sections (warn only)
local body
body=$(awk 'BEGIN{n=0} /^---$/{n++; next} n>=2{print}' "$file")
for section in "${RECOMMENDED_SECTIONS[@]}"; do
if ! echo "$body" | grep -qi "$section"; then
echo "WARN $file: missing recommended section '${section}'"
warnings=$((warnings + 1))
fi
done
# 4. Check file has meaningful content
if [[ $(echo "$body" | wc -w) -lt 50 ]]; then
echo "WARN $file: body seems very short (< 50 words)"
warnings=$((warnings + 1))
fi
}
# Collect files to lint
files=()
if [[ $# -gt 0 ]]; then
files=("$@")
else
for dir in "${AGENT_DIRS[@]}"; do
if [[ -d "$dir" ]]; then
while IFS= read -r f; do
files+=("$f")
done < <(find "$dir" -name "*.md" -type f | sort)
fi
done
fi
if [[ ${#files[@]} -eq 0 ]]; then
echo "No agent files found."
exit 1
fi
echo "Linting ${#files[@]} agent files..."
echo ""
for file in "${files[@]}"; do
lint_file "$file"
done
echo ""
echo "Results: ${errors} error(s), ${warnings} warning(s) in ${#files[@]} files."
if [[ $errors -gt 0 ]]; then
echo "FAILED: fix the errors above before merging."
exit 1
else
echo "PASSED"
exit 0
fi