#!/bin/bash # expand-categories.sh — Expand skill categories into full manifest # # Uses claude --print to expand each category in categories.json # into 15-25 individual skill titles, producing manifest.json # # Usage: # ./expand-categories.sh # Expand all categories # ./expand-categories.sh azure-ai-services # Expand single category # ./expand-categories.sh --wave 1 # Expand wave 1 only # # Prerequisites: # - claude CLI installed and authenticated # - jq installed # - categories.json in same directory set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" CATEGORIES_FILE="$SCRIPT_DIR/categories.json" MANIFEST_FILE="$SCRIPT_DIR/manifest.json" LOG_DIR="$SCRIPT_DIR/logs" # Model for expansion (haiku is sufficient for generating titles) MODEL="${MODEL:-haiku}" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log() { echo -e "${BLUE}[expand]${NC} $1" >&2; } success() { echo -e "${GREEN}[expand]${NC} $1" >&2; } warn() { echo -e "${YELLOW}[expand]${NC} $1" >&2; } error() { echo -e "${RED}[expand]${NC} $1" >&2; } # Check prerequisites check_prereqs() { if ! command -v claude &>/dev/null; then error "claude CLI not found. Install: npm install -g @anthropic-ai/claude-code" exit 1 fi if ! command -v jq &>/dev/null; then error "jq not found. Install: brew install jq" exit 1 fi if [[ ! -f "$CATEGORIES_FILE" ]]; then error "categories.json not found at $CATEGORIES_FILE" exit 1 fi } # Get list of existing reference files for context get_existing_refs() { local category_dir="$1" local refs_dir="$PLUGIN_DIR/skills/ms-ai-engineering/references" # List all existing reference files find "$refs_dir" -name "*.md" -type f 2>/dev/null | while read -r f; do basename "$f" .md done | sort | tr '\n' ', ' } # Expand a single category expand_category() { local category_key="$1" local name description estimated examples existing_overlap notes name=$(jq -r ".categories[\"$category_key\"].name" "$CATEGORIES_FILE") description=$(jq -r ".categories[\"$category_key\"].description" "$CATEGORIES_FILE") estimated=$(jq -r ".categories[\"$category_key\"].estimated_skills" "$CATEGORIES_FILE") examples=$(jq -r ".categories[\"$category_key\"].examples | join(\", \")" "$CATEGORIES_FILE") existing_overlap=$(jq -r ".categories[\"$category_key\"].existing_overlap | join(\", \")" "$CATEGORIES_FILE") notes=$(jq -r ".categories[\"$category_key\"].notes" "$CATEGORIES_FILE") local existing_refs existing_refs=$(get_existing_refs "$category_key") log "Expanding: $name ($estimated skills estimated)" local prompt prompt="Du er en Microsoft AI Solution Architect som planlegger en kunnskapsbase. Kategorien **${name}** trenger individuelle kunnskapsfiler (skills). ## Kategori-beskrivelse ${description} ## Eksisterende filer i kunnskapsbasen (unngå duplikering) ${existing_refs} ## Eksisterende overlapp å ta hensyn til ${existing_overlap} ## Eksempel-titler (for inspirasjon, ikke begrens deg til disse) ${examples} ## Notater ${notes} ## Oppgave Generer en JSON-array med NØYAKTIG ${estimated} skills for denne kategorien. Hver skill skal ha: - \`id\`: kebab-case filnavn (uten .md) - \`title\`: Engelsk tittel (kortfattet, beskrivende) - \`description\`: 1-2 setninger på norsk om hva filen dekker - \`subtopics\`: 3-5 viktige undertemaer som array Regler: 1. Ikke dupliser emner som allerede finnes i eksisterende filer 2. Sørg for bred dekning uten overlapp mellom skills 3. Titler skal være spesifikke (\"Azure AI Vision OCR and Document Processing\", ikke bare \"Vision\") 4. Prioriter mest nyttige emner for enterprise AI-arkitekter i norsk offentlig sektor 5. Returner KUN valid JSON-array, ingen annen tekst Eksempel-format: [ { \"id\": \"example-skill-name\", \"title\": \"Example Skill - Full Descriptive Title\", \"description\": \"Beskrivelse av hva denne kunnskapsfilen dekker.\", \"subtopics\": [\"subtopic-1\", \"subtopic-2\", \"subtopic-3\"] } ]" local output output=$(claude --print --model "$MODEL" "$prompt" 2>"$LOG_DIR/expand-${category_key}.err") # Extract JSON array from response (handles markdown code blocks, plain JSON, etc.) local json_output json_output=$(python3 -c " import sys, json, re text = sys.stdin.read() # Try to find JSON array in code blocks first m = re.search(r'\`\`\`(?:json)?\s*(\[[\s\S]*?\])\s*\`\`\`', text) if m: arr = json.loads(m.group(1)) print(json.dumps(arr)) sys.exit(0) # Try to find bare JSON array m = re.search(r'(\[[\s\S]*\])', text) if m: try: arr = json.loads(m.group(1)) print(json.dumps(arr)) sys.exit(0) except: pass # Nothing found sys.exit(1) " <<< "$output" 2>/dev/null) # Validate JSON if ! echo "$json_output" | jq . &>/dev/null; then error "Invalid JSON for $category_key. Raw output saved to $LOG_DIR/expand-${category_key}.raw" echo "$output" > "$LOG_DIR/expand-${category_key}.raw" return 1 fi local count count=$(echo "$json_output" | jq 'length') success "$name: $count skills generated" # Return the JSON echo "$json_output" } # Build or update manifest build_manifest() { local categories_to_expand=("$@") # Initialize manifest if it doesn't exist if [[ ! -f "$MANIFEST_FILE" ]]; then echo '{"version":"1.0","created":"'"$(date +%Y-%m-%d)"'","categories":{}}' | jq . > "$MANIFEST_FILE" fi local total=0 local failed=0 for category_key in "${categories_to_expand[@]}"; do local skills_json if skills_json=$(expand_category "$category_key"); then # Add to manifest local dir dir=$(jq -r ".categories[\"$category_key\"].dir" "$CATEGORIES_FILE") local name name=$(jq -r ".categories[\"$category_key\"].name" "$CATEGORIES_FILE") local priority priority=$(jq -r ".categories[\"$category_key\"].priority" "$CATEGORIES_FILE") local category_obj category_obj=$(jq -n \ --arg name "$name" \ --arg dir "$dir" \ --arg priority "$priority" \ --argjson skills "$skills_json" \ '{name: $name, dir: $dir, priority: $priority, skills: $skills}') # Merge into manifest jq --arg key "$category_key" --argjson cat "$category_obj" \ '.categories[$key] = $cat' "$MANIFEST_FILE" > "$MANIFEST_FILE.tmp" \ && mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE" local count count=$(echo "$skills_json" | jq 'length') total=$((total + count)) else failed=$((failed + 1)) fi # Rate limiting: pause between API calls sleep 2 done echo "" >&2 log "═══════════════════════════════════════" success "Total skills in manifest: $total" [[ $failed -gt 0 ]] && error "Failed categories: $failed" log "Manifest: $MANIFEST_FILE" log "═══════════════════════════════════════" } # Parse arguments parse_args() { local wave="" local single_category="" while [[ $# -gt 0 ]]; do case "$1" in --wave) wave="$2" shift 2 ;; --model) MODEL="$2" shift 2 ;; --help|-h) echo "Usage: $0 [category-key] [--wave N] [--model MODEL]" echo "" echo "Options:" echo " category-key Expand single category" echo " --wave N Expand all categories in wave N (1 or 2)" echo " --model MODEL Claude model to use (default: haiku)" echo "" echo "Examples:" echo " $0 # Expand all categories" echo " $0 azure-ai-services # Expand single category" echo " $0 --wave 1 # Expand HIGH priority only" exit 0 ;; *) single_category="$1" shift ;; esac done # Determine which categories to expand if [[ -n "$single_category" ]]; then echo "$single_category" elif [[ -n "$wave" ]]; then jq -r ".waves[] | select(.wave == $wave) | .categories[]" "$CATEGORIES_FILE" else jq -r '.categories | keys[]' "$CATEGORIES_FILE" fi } # Main main() { check_prereqs mkdir -p "$LOG_DIR" log "Skill Category Expansion" log "Model: $MODEL | Categories file: $CATEGORIES_FILE" echo "" >&2 local categories=() while IFS= read -r line; do categories+=("$line") done < <(parse_args "$@") if [[ ${#categories[@]} -eq 0 ]]; then error "No categories to expand" exit 1 fi log "Categories to expand: ${#categories[@]}" for cat in "${categories[@]}"; do log " - $cat" done echo "" >&2 build_manifest "${categories[@]}" } main "$@"