#!/bin/bash set -e # Generate agent skill from Hindsight documentation # Converts docs/ to skills/hindsight-docs/ for AI agent consumption SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(dirname "$SCRIPT_DIR")" DOCS_DIR="$ROOT_DIR/hindsight-docs/docs" PAGES_DIR="$ROOT_DIR/hindsight-docs/src/pages" EXAMPLES_DIR="$ROOT_DIR/hindsight-docs/examples" SKILL_DIR="$ROOT_DIR/skills/hindsight-docs" REFS_DIR="$SKILL_DIR/references" # Colors GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' print_info() { echo -e "${GREEN}[INFO]${NC} $1" } print_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } print_info "Generating Hindsight documentation skill..." # Clean and recreate skill directory rm -rf "$SKILL_DIR" mkdir -p "$REFS_DIR" # Process markdown files process_file() { local src_file="$1" local rel_path="${src_file#$DOCS_DIR/}" local dest_file="$REFS_DIR/$rel_path" # Create destination directory mkdir -p "$(dirname "$dest_file")" # Process the file if [[ "$src_file" == *.mdx ]]; then # Change .mdx to .md dest_file="${dest_file%.mdx}.md" print_info "Converting: $rel_path" convert_mdx_to_md "$src_file" "$dest_file" else print_info "Copying: $rel_path" cp "$src_file" "$dest_file" fi } # Convert MDX to Markdown by: # 1. Removing import statements # 2. Replacing JSX components with markdown equivalents # 3. Inlining code examples from example files convert_mdx_to_md() { local src="$1" local dest="$2" # Use Python for more robust processing python3 - "$src" "$dest" "$EXAMPLES_DIR" <<'PYTHON' import sys import re from pathlib import Path src_file = Path(sys.argv[1]) dest_file = Path(sys.argv[2]) examples_dir = Path(sys.argv[3]) content = src_file.read_text() original_content = content # Keep original for import searches # Remove frontmatter content = re.sub(r'^---\n.*?\n---\n', '', content, flags=re.DOTALL) # Remove import statements content = re.sub(r'^import .*?;?\n', '', content, flags=re.MULTILINE) # Extract code example inlining: # Replace with actual code content from examples directory def inline_code_snippet(match): var_name = match.group(1) section = match.group(2) language = match.group(3) # Find the import that loaded this variable - search in original content import_match = re.search(rf"import {var_name} from '!!raw-loader!@site/(.+?)';", original_content) if not import_match: return f"```{language}\n# Could not find import for: {var_name}\n```" # Load the example file # The import path is like "examples/api/quickstart.py", but examples_dir already points to examples/ example_rel_path = import_match.group(1) # Strip "examples/" prefix if present since examples_dir already includes it if example_rel_path.startswith("examples/"): example_rel_path = example_rel_path[len("examples/"):] example_path = examples_dir / example_rel_path if not example_path.exists(): return f"```{language}\n# Example file not found: {example_path}\n```" example_content = example_path.read_text() # Extract section if specified - examples use comment markers like # [docs:section] or // [docs:section] if section: # Try various comment formats: #, //, etc. # Pattern: (comment) [docs:section] ... (comment) [/docs:section] section_pattern = rf"(?:^|\n)(?:#|//)\s*\[docs:{re.escape(section)}\]\n(.*?)\n(?:#|//)\s*\[/docs:{re.escape(section)}\]" section_match = re.search(section_pattern, example_content, re.DOTALL | re.MULTILINE) if not section_match: # Try alternative # section-start / # section-end format section_pattern = rf"(?:^|\n)#\s*{re.escape(section)}-start\n(.*?)\n#\s*{re.escape(section)}-end" section_match = re.search(section_pattern, example_content, re.DOTALL | re.MULTILINE) if section_match: example_content = section_match.group(1).strip() else: return f"```{language}\n# Section '{section}' not found in {example_rel_path}\n```" return f"```{language}\n{example_content}\n```" content = re.sub( r'', inline_code_snippet, content ) # Convert to markdown sections # Replace ... with markdown headers content = re.sub(r'\s*', '', content) content = re.sub(r'\s*', '', content) # Convert to ### Y content = re.sub(r'', r'### \1\n', content) content = re.sub(r'', '', content) # Convert :::tip, :::warning, :::note to markdown blockquotes content = re.sub(r':::tip (.+?)\n', r'> **💡 \1**\n> \n', content) content = re.sub(r':::warning (.+?)\n', r'> **⚠️ \1**\n> \n', content) content = re.sub(r':::note (.+?)\n', r'> **📝 \1**\n> \n', content) content = re.sub(r':::\s*\n', '', content) # Clean up extra blank lines content = re.sub(r'\n{3,}', '\n\n', content) dest_file.write_text(content) PYTHON } # Find and process all markdown files print_info "Processing documentation files..." find "$DOCS_DIR" -type f \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do process_file "$file" done # Process standalone pages (e.g. best-practices, faq) from src/pages/ print_info "Processing standalone pages..." for page in best-practices faq; do for ext in md mdx; do src="$PAGES_DIR/$page.$ext" if [ -f "$src" ]; then dest="$REFS_DIR/$page.md" mkdir -p "$(dirname "$dest")" if [[ "$src" == *.mdx ]]; then convert_mdx_to_md "$src" "$dest" else cp "$src" "$dest" fi print_info "Included page: $page.$ext" fi done done # Process changelog — may be a single file or a directory if [ -f "$PAGES_DIR/changelog.md" ] || [ -f "$PAGES_DIR/changelog.mdx" ]; then for ext in md mdx; do src="$PAGES_DIR/changelog.$ext" if [ -f "$src" ]; then dest="$REFS_DIR/changelog.md" mkdir -p "$(dirname "$dest")" if [[ "$src" == *.mdx ]]; then convert_mdx_to_md "$src" "$dest" else cp "$src" "$dest" fi print_info "Included page: changelog.$ext" fi done elif [ -d "$PAGES_DIR/changelog" ]; then find "$PAGES_DIR/changelog" -type f \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do rel="${file#$PAGES_DIR/}" dest="$REFS_DIR/$rel" if [[ "$file" == *.mdx ]]; then dest="${dest%.mdx}.md" fi mkdir -p "$(dirname "$dest")" if [[ "$file" == *.mdx ]]; then convert_mdx_to_md "$file" "$dest" else cp "$file" "$dest" fi print_info "Included changelog: ${file#$PAGES_DIR/changelog/}" done fi # Copy OpenAPI spec into the skill OPENAPI_SRC="$ROOT_DIR/hindsight-docs/static/openapi.json" if [ -f "$OPENAPI_SRC" ]; then cp "$OPENAPI_SRC" "$REFS_DIR/openapi.json" print_info "Included: openapi.json" else print_warn "openapi.json not found at $OPENAPI_SRC — skipping" fi # Generate SKILL.md print_info "Generating SKILL.md..." cat > "$SKILL_DIR/SKILL.md" <<'EOF' --- name: hindsight-docs description: Complete Hindsight documentation for AI agents. Use this to learn about Hindsight architecture, APIs, configuration, and best practices. --- # Hindsight Documentation Skill Complete technical documentation for Hindsight - a biomimetic memory system for AI agents. ## When to Use This Skill Use this skill when you need to: - Understand Hindsight architecture and core concepts - Learn about retain/recall/reflect operations - Configure memory banks and dispositions - Set up the Hindsight API server (Docker, Kubernetes, pip) - Integrate with Python/Node.js/Rust SDKs - Understand retrieval strategies (semantic, BM25, graph, temporal) - Debug issues or optimize performance - Review API endpoints and parameters - Find cookbook examples and recipes ## Documentation Structure All documentation is in `references/` organized by category: ``` references/ ├── best-practices.md # START HERE — missions, tags, formats, anti-patterns ├── faq.md # Common questions and decisions ├── changelog/ # Release history and version changes (index.md + integrations/) ├── openapi.json # Full OpenAPI spec — endpoint schemas, request/response models ├── developer/ │ ├── api/ # Core operations: retain, recall, reflect, memory banks │ └── *.md # Architecture, configuration, deployment, performance ├── sdks/ │ ├── *.md # Python, Node.js, CLI, embedded │ └── integrations/ # LiteLLM, AI SDK, OpenClaw, MCP, skills └── cookbook/ ├── recipes/ # Usage patterns and examples └── applications/ # Full application demos ``` ## How to Find Documentation ### 1. Find Files by Pattern (use Glob tool) ```bash # Core API operations references/developer/api/*.md # SDK documentation references/sdks/*.md references/sdks/integrations/*.md # Cookbook examples references/cookbook/recipes/*.md references/cookbook/applications/*.md # Find specific topics references/**/configuration.md references/**/*python*.md references/**/*deployment*.md ``` ### 2. Search Content (use Grep tool) ```bash # Search for concepts pattern: "disposition" # Memory bank configuration pattern: "graph retrieval" # Graph-based search pattern: "helm install" # Kubernetes deployment pattern: "document_id" # Document management pattern: "HINDSIGHT_API_" # Environment variables # Search in specific areas path: references/developer/api/ pattern: "POST /v1" # Find API endpoints path: references/cookbook/ pattern: "def |async def " # Find Python examples ``` ### 3. Read Full Documentation (use Read tool) ``` references/developer/api/retain.md references/sdks/python.md references/cookbook/recipes/per-user-memory.md ``` ## Start Here: Best Practices Before reading API docs, read the best practices guide. It covers practical rules for missions, tags, content format, observation scopes, and anti-patterns — the fastest way to integrate correctly. ``` references/best-practices.md ``` ## Key Concepts - **Memory Banks**: Isolated memory stores (one per user/agent) - **Retain**: Store memories (auto-extracts facts/entities/relationships) - **Recall**: Retrieve memories (4 parallel strategies: semantic, BM25, graph, temporal) - **Reflect**: Disposition-aware reasoning using memories - **document_id**: Groups messages in a conversation (upsert on same ID) - **Dispositions**: Skepticism, literalism, empathy traits (1-5) affecting reflect - **Mental Models**: Consolidated knowledge synthesized from facts ## Notes - Code examples are inlined from working examples - Configuration uses `HINDSIGHT_API_*` environment variables - Database migrations run automatically on startup - Multi-bank queries require client-side orchestration - Use `document_id` for conversation evolution (same ID = upsert) --- **Auto-generated** from `hindsight-docs/docs/`. Run `./scripts/generate-docs-skill.sh` to update. EOF print_info "✓ Generated skill at: $SKILL_DIR" print_info "✓ Documentation files: $(find "$REFS_DIR" -type f | wc -l | tr -d ' ')" print_info "✓ SKILL.md created with search guidance" # Rewrite Docusaurus absolute paths (e.g. /developer/foo) to relative paths print_info "Rewriting Docusaurus absolute paths to relative paths..." python3 - "$REFS_DIR" <<'PYTHON' import sys import re import os from pathlib import Path refs_dir = Path(sys.argv[1]).resolve() link_pattern = re.compile(r'\[([^\]]*)\]\((/[^)]*)\)') SPECIAL_MAPPINGS = { '/api-reference': 'openapi.json', } def try_resolve(url_path, refs_dir): """Try to find the file in refs_dir for a Docusaurus absolute path like /developer/foo.""" if url_path in SPECIAL_MAPPINGS: candidate = refs_dir / SPECIAL_MAPPINGS[url_path] return candidate if candidate.exists() else None doc_path = url_path.lstrip('/') for candidate in [ refs_dir / (doc_path + '.md'), refs_dir / doc_path / 'index.md', refs_dir / doc_path, ]: if candidate.exists(): return candidate return None image_pattern = re.compile(r'!\[[^\]]*\]\([^)]*\)') html_img_pattern = re.compile(r']*/?>', re.IGNORECASE) changed = 0 for md_file in refs_dir.rglob("*.md"): original_content = md_file.read_text() # Strip images (markdown and HTML) content = image_pattern.sub('', original_content) content = html_img_pattern.sub('', content) def rewrite(match): text = match.group(1) url = match.group(2) anchor = '' if '#' in url: url, frag = url.split('#', 1) anchor = '#' + frag if not url or url == '/': return text # strip link, keep text resolved = try_resolve(url, refs_dir) if resolved is None: return text # strip unresolvable link, keep text rel = os.path.relpath(resolved, md_file.parent) return f'[{text}]({rel}{anchor})' new_content = link_pattern.sub(rewrite, content) if new_content != original_content: md_file.write_text(new_content) changed += 1 print(f"[INFO] Rewrote Docusaurus links in {changed} file(s)") PYTHON # Validate: no links point outside the skill directory print_info "Validating links in generated skill files..." python3 - "$SKILL_DIR" <<'PYTHON' import sys import re from pathlib import Path skill_dir = Path(sys.argv[1]).resolve() errors = [] # Find all markdown links: [text](url) — exclude images too link_pattern = re.compile(r'\[([^\]]*)\]\(([^)]+)\)') for md_file in skill_dir.rglob("*.md"): content = md_file.read_text() for match in link_pattern.finditer(content): url = match.group(2).split("#")[0].strip() # strip anchors if not url: continue # Absolute URLs and anchors-only are fine if url.startswith(("http://", "https://", "mailto:", "ftp://")): continue # Resolve relative to the file's directory resolved = (md_file.parent / url).resolve() if not str(resolved).startswith(str(skill_dir)): errors.append(f" {md_file.relative_to(skill_dir)}: '{url}' -> {resolved}") if errors: print("ERROR: The following links point outside the skill directory.") print("All links must be absolute URLs or relative paths within the skill.") for e in errors: print(e) sys.exit(1) print(f"[INFO] Link validation passed ({skill_dir})") PYTHON echo "" print_info "Usage:" echo " - Agents can use Glob to find files: references/developer/api/*.md" echo " - Agents can use Grep to search content: pattern='disposition'" echo " - Agents can use Read to view full docs"