Skill: xenofex7/bookstack (version 1.0.3)

--- name: bookstack description: "BookStack Wiki & Documentation API integration. Manage your knowledge base programmatically: create, read, update, and delete books, chapters, pages, and shelves. Full-text search across all content. Use when you need to: (1) Create or edit wiki pages and documentation, (2) Organize content in books and chapters, (3) Search your knowledge base, (4) Automate documentation workflows, (5) Sync content between systems. Supports both HTML and Markdown content." metadata: openclaw: requires: env: - BOOKSTACK_URL - BOOKSTACK_TOKEN_ID - BOOKSTACK_TOKEN_SECRET --- # BookStack Skill **BookStack** is an open-source wiki and documentation platform. This skill lets you manage your entire knowledge base via API – perfect for automation and integration. ## Features - 📚 **Books** – create, edit, delete - 📑 **Chapters** – organize content within books - 📄 **Pages** – create/edit with HTML or Markdown - 🔍 **Full-text search** – search across all content - 📁 **Shelves** – organize books into collections ## Quick Start ```bash # List all books python3 scripts/bookstack.py list_books # Search the knowledge base python3 scripts/bookstack.py search "Home Assistant" # Get a page python3 scripts/bookstack.py get_page 123 # Create a new page (Markdown) python3 scripts/bookstack.py create_page --book-id 1 --name "My Page" --markdown "# Title\n\nContent here..." ``` ## All Commands ### Books ```bash python3 scripts/bookstack.py list_books # List all books python3 scripts/bookstack.py get_book <id> # Book details python3 scripts/bookstack.py create_book "Name" ["Desc"] # New book python3 scripts/bookstack.py update_book <id> [--name] [--description] python3 scripts/bookstack.py delete_book <id> ``` ### Chapters ```bash python3 scripts/bookstack.py list_chapters # List all chapters python3 scripts/bookstack.py get_chapter <id> # Chapter details python3 scripts/bookstack.py create_chapter --book-id <id> --name "Name" python3 scripts/bookstack.py update_chapter <id> [--name] [--description] python3 scripts/bookstack.py delete_chapter <id> ``` ### Pages ```bash python3 scripts/bookstack.py list_pages # List all pages python3 scripts/bookstack.py get_page <id> # Page preview python3 scripts/bookstack.py get_page <id> --content # With HTML content python3 scripts/bookstack.py get_page <id> --markdown # As Markdown # Create page (in book or chapter) python3 scripts/bookstack.py create_page --book-id <id> --name "Name" --markdown "# Content" python3 scripts/bookstack.py create_page --chapter-id <id> --name "Name" --html "<p>HTML</p>" # Edit page python3 scripts/bookstack.py update_page <id> [--name] [--content] [--markdown] python3 scripts/bookstack.py delete_page <id> ``` ### Search ```bash python3 scripts/bookstack.py search "query" # Search everything python3 scripts/bookstack.py search "query" --type page # Pages only python3 scripts/bookstack.py search "query" --type book # Books only ``` ### Shelves ```bash python3 scripts/bookstack.py list_shelves # List all shelves python3 scripts/bookstack.py get_shelf <id> # Shelf details python3 scripts/bookstack.py create_shelf "Name" ["Desc"] # New shelf ``` ## Configuration Set the following environment variables: ```bash export BOOKSTACK_URL="https://your-bookstack.example.com" export BOOKSTACK_TOKEN_ID="your-token-id" export BOOKSTACK_TOKEN_SECRET="your-token-secret" ``` Or configure via your gateway config file under `skills.entries.bookstack.env`. ### Create an API Token 1. Log in to your BookStack instance 2. Go to **Edit Profile** → **API Tokens** 3. Click **Create Token** 4. Copy the Token ID and Secret ⚠️ The user needs a role with **"Access System API"** permission! ## API Reference - **Base URL**: `{BOOKSTACK_URL}/api` - **Auth Header**: `Authorization: Token {ID}:{SECRET}` - **Official Docs**: https://demo.bookstackapp.com/api/docs --- **Author**: xenofex7 | **Version**: 1.0.2

path: _meta.json
content: ownerId: kn74cct4grf5r7mhc6rw5056e17z60s0
slug: bookstack
version: 1.0.1
publishedAt: 1769088047496

path: scripts/bookstack.py
content: #!/usr/bin/env python3
"""
BookStack API Integration
Full CRUD for books, chapters, pages, shelves + search
"""

import argparse
import json
import os
import sys
import urllib.request
import urllib.error
import urllib.parse

# Configuration from environment
BASE_URL = os.getenv('BOOKSTACK_URL', '').rstrip('/')
TOKEN_ID = os.getenv('BOOKSTACK_TOKEN_ID', '')
TOKEN_SECRET = os.getenv('BOOKSTACK_TOKEN_SECRET', '')

def api_call(method, endpoint, data=None, params=None):
    """Make API call to BookStack"""
    if not BASE_URL or not TOKEN_ID or not TOKEN_SECRET:
        print("❌ Error: BOOKSTACK_URL, BOOKSTACK_TOKEN_ID, and BOOKSTACK_TOKEN_SECRET required!")
        print("   Set them as environment variables or in your gateway config.")
        sys.exit(1)
    
    url = f"{BASE_URL}/api/{endpoint}"
    
    if params:
        url += '?' + urllib.parse.urlencode(params)
    
    try:
        req = urllib.request.Request(
            url,
            headers={
                "Authorization": f"Token {TOKEN_ID}:{TOKEN_SECRET}",
                "Content-Type": "application/json",
                "Accept": "application/json"
            },
            method=method
        )
        
        if data:
            data = {k: v for k, v in data.items() if v is not None}
            req.data = json.dumps(data).encode()
        
        with urllib.request.urlopen(req, timeout=30) as response:
            if response.status == 204:
                return None
            return json.loads(response.read().decode())
    
    except urllib.error.HTTPError as e:
        try:
            error_data = json.loads(e.read().decode())
            print(f"❌ HTTP {e.code}: {error_data.get('error', {}).get('message', 'Unknown error')}")
        except:
            print(f"❌ HTTP {e.code}: {e.reason}")
        sys.exit(1)
    except urllib.error.URLError as e:
        print(f"❌ Connection error: {e.reason}")
        sys.exit(1)
    except Exception as e:
        print(f"❌ Error: {e}")
        sys.exit(1)

# ============ BOOKS ============

def list_books(args):
    """List all books"""
    params = {'count': args.count} if args.count else {}
    result = api_call("GET", "books", params=params)
    
    if not result.get('data'):
        print("📚 No books found")
        return
    
    print(f"📚 {result.get('total', len(result['data']))} Books:\n")
    for book in result['data']:
        desc = book.get('description', '')[:50] + '...' if book.get('description') else ''
        print(f"  [{book['id']}] {book['name']}")
        if desc:
            print(f"       {desc}")

def get_book(args):
    """Get book details"""
    result = api_call("GET", f"books/{args.id}")
    print(f"📚 Book: {result['name']}")
    print(f"   ID: {result['id']}")
    print(f"   Slug: {result['slug']}")
    if result.get('description'):
        print(f"   Description: {result['description']}")
    print(f"   Created: {result['created_at']}")
    print(f"   Updated: {result['updated_at']}")
    if result.get('contents'):
        print(f"\n   Contents ({len(result['contents'])} items):")
        for item in result['contents'][:10]:
            icon = '📑' if item['type'] == 'chapter' else '📄'
            print(f"     {icon} [{item['id']}] {item['name']}")

def create_book(args):
    """Create a new book"""
    data = {
        "name": args.name,
        "description": args.description
    }
    result = api_call("POST", "books", data)
    print(f"✅ Book created: {result['name']} (ID: {result['id']})")

def update_book(args):
    """Update a book"""
    data = {}
    if args.name:
        data['name'] = args.name
    if args.description:
        data['description'] = args.description
    
    if not data:
        print("❌ Nothing to update. Use --name or --description")
        sys.exit(1)
    
    result = api_call("PUT", f"books/{args.id}", data)
    print(f"✅ Book updated: {result['name']}")

def delete_book(args):
    """Delete a book"""
    api_call("DELETE", f"books/{args.id}")
    print(f"✅ Book {args.id} deleted")

# ============ CHAPTERS ============

def list_chapters(args):
    """List all chapters"""
    params = {'count': args.count} if args.count else {}
    result = api_call("GET", "chapters", params=params)
    
    if not result.get('data'):
        print("📑 No chapters found")
        return
    
    print(f"📑 {result.get('total', len(result['data']))} Chapters:\n")
    for ch in result['data']:
        print(f"  [{ch['id']}] {ch['name']} (Book: {ch.get('book_id', '?')})")

def get_chapter(args):
    """Get chapter details"""
    result = api_call("GET", f"chapters/{args.id}")
    print(f"📑 Chapter: {result['name']}")
    print(f"   ID: {result['id']}")
    print(f"   Book ID: {result['book_id']}")
    if result.get('description'):
        print(f"   Description: {result['description']}")
    if result.get('pages'):
        print(f"\n   Pages ({len(result['pages'])}):")
        for page in result['pages'][:10]:
            print(f"     📄 [{page['id']}] {page['name']}")

def create_chapter(args):
    """Create a new chapter"""
    data = {
        "book_id": args.book_id,
        "name": args.name,
        "description": args.description
    }
    result = api_call("POST", "chapters", data)
    print(f"✅ Chapter created: {result['name']} (ID: {result['id']})")

def update_chapter(args):
    """Update a chapter"""
    data = {}
    if args.name:
        data['name'] = args.name
    if args.description:
        data['description'] = args.description
    if args.book_id:
        data['book_id'] = args.book_id
    
    if not data:
        print("❌ Nothing to update")
        sys.exit(1)
    
    result = api_call("PUT", f"chapters/{args.id}", data)
    print(f"✅ Chapter updated: {result['name']}")

def delete_chapter(args):
    """Delete a chapter"""
    api_call("DELETE", f"chapters/{args.id}")
    print(f"✅ Chapter {args.id} deleted")

# ============ PAGES ============

def list_pages(args):
    """List all pages"""
    params = {'count': args.count} if args.count else {}
    result = api_call("GET", "pages", params=params)
    
    if not result.get('data'):
        print("📄 No pages found")
        return
    
    print(f"📄 {result.get('total', len(result['data']))} Pages:\n")
    for page in result['data']:
        location = f"Chapter {page['chapter_id']}" if page.get('chapter_id') else f"Book {page['book_id']}"
        print(f"  [{page['id']}] {page['name']} ({location})")

def get_page(args):
    """Get page with full content"""
    result = api_call("GET", f"pages/{args.id}")
    print(f"📄 Page: {result['name']}")
    print(f"   ID: {result['id']}")
    print(f"   Book ID: {result['book_id']}")
    if result.get('chapter_id'):
        print(f"   Chapter ID: {result['chapter_id']}")
    print(f"   Editor: {result.get('editor', 'unknown')}")
    print(f"   Created: {result['created_at']}")
    print(f"   Updated: {result['updated_at']}")
    
    if args.content:
        print(f"\n--- Content (HTML) ---")
        print(result.get('html', ''))
    elif args.markdown:
        print(f"\n--- Content (Markdown) ---")
        print(result.get('markdown', result.get('html', '')))
    else:
        # Show preview
        html = result.get('html', '')
        if html:
            # Strip HTML tags for preview
            import re
            text = re.sub('<[^<]+?>', '', html)
            text = ' '.join(text.split())[:200]
            print(f"\n   Preview: {text}...")

def create_page(args):
    """Create a new page"""
    data = {
        "name": args.name,
    }
    
    if args.book_id:
        data['book_id'] = args.book_id
    if args.chapter_id:
        data['chapter_id'] = args.chapter_id
    
    if not args.book_id and not args.chapter_id:
        print("❌ Either --book-id or --chapter-id required")
        sys.exit(1)
    
    if args.html:
        data['html'] = args.html
    elif args.markdown:
        data['markdown'] = args.markdown
    elif args.content:
        # Auto-detect: if starts with # or no HTML tags, treat as markdown
        if args.content.startswith('#') or '<' not in args.content:
            data['markdown'] = args.content
        else:
            data['html'] = args.content
    
    result = api_call("POST", "pages", data)
    print(f"✅ Page created: {result['name']} (ID: {result['id']})")

def update_page(args):
    """Update a page"""
    data = {}
    if args.name:
        data['name'] = args.name
    if args.html:
        data['html'] = args.html
    if args.markdown:
        data['markdown'] = args.markdown
    if args.content:
        if args.content.startswith('#') or '<' not in args.content:
            data['markdown'] = args.content
        else:
            data['html'] = args.content
    if args.book_id:
        data['book_id'] = args.book_id
    if args.chapter_id:
        data['chapter_id'] = args.chapter_id
    
    if not data:
        print("❌ Nothing to update")
        sys.exit(1)
    
    result = api_call("PUT", f"pages/{args.id}", data)
    print(f"✅ Page updated: {result['name']}")

def delete_page(args):
    """Delete a page"""
    api_call("DELETE", f"pages/{args.id}")
    print(f"✅ Page {args.id} deleted")

# ============ SHELVES ============

def list_shelves(args):
    """List all shelves"""
    params = {'count': args.count} if args.count else {}
    result = api_call("GET", "shelves", params=params)
    
    if not result.get('data'):
        print("📁 No shelves found")
        return
    
    print(f"📁 {result.get('total', len(result['data']))} Shelves:\n")
    for shelf in result['data']:
        print(f"  [{shelf['id']}] {shelf['name']}")

def get_shelf(args):
    """Get shelf details"""
    result = api_call("GET", f"shelves/{args.id}")
    print(f"📁 Shelf: {result['name']}")
    print(f"   ID: {result['id']}")
    if result.get('description'):
        print(f"   Description: {result['description']}")
    if result.get('books'):
        print(f"\n   Books ({len(result['books'])}):")
        for book in result['books'][:10]:
            print(f"     📚 [{book['id']}] {book['name']}")

def create_shelf(args):
    """Create a new shelf"""
    data = {
        "name": args.name,
        "description": args.description
    }
    result = api_call("POST", "shelves", data)
    print(f"✅ Shelf created: {result['name']} (ID: {result['id']})")

# ============ SEARCH ============

def search(args):
    """Search content"""
    params = {
        'query': args.query,
        'count': args.count or 20
    }
    if args.type:
        params['query'] = f"{{{args.type}}} {args.query}"
    
    result = api_call("GET", "search", params=params)
    
    if not result.get('data'):
        print(f"🔍 No results for: {args.query}")
        return
    
    print(f"🔍 {result.get('total', len(result['data']))} Results for '{args.query}':\n")
    for item in result['data']:
        icon = {'page': '📄', 'chapter': '📑', 'book': '📚', 'bookshelf': '📁'}.get(item['type'], '📎')
        print(f"  {icon} [{item['type']}:{item['id']}] {item['name']}")
        if item.get('preview_html'):
            import re
            preview = re.sub('<[^<]+?>', '', item['preview_html'].get('content', ''))
            preview = ' '.join(preview.split())[:80]
            if preview:
                print(f"       {preview}...")

# ============ MAIN ============

def main():
    parser = argparse.ArgumentParser(description='BookStack API CLI')
    subparsers = parser.add_subparsers(dest='command', help='Commands')
    
    # Books
    p = subparsers.add_parser('list_books', help='List all books')
    p.add_argument('--count', type=int, help='Max results')
    p.set_defaults(func=list_books)
    
    p = subparsers.add_parser('get_book', help='Get book details')
    p.add_argument('id', type=int)
    p.set_defaults(func=get_book)
    
    p = subparsers.add_parser('create_book', help='Create a book')
    p.add_argument('name')
    p.add_argument('description', nargs='?')
    p.set_defaults(func=create_book)
    
    p = subparsers.add_parser('update_book', help='Update a book')
    p.add_argument('id', type=int)
    p.add_argument('--name')
    p.add_argument('--description')
    p.set_defaults(func=update_book)
    
    p = subparsers.add_parser('delete_book', help='Delete a book')
    p.add_argument('id', type=int)
    p.set_defaults(func=delete_book)
    
    # Chapters
    p = subparsers.add_parser('list_chapters', help='List all chapters')
    p.add_argument('--count', type=int)
    p.set_defaults(func=list_chapters)
    
    p = subparsers.add_parser('get_chapter', help='Get chapter details')
    p.add_argument('id', type=int)
    p.set_defaults(func=get_chapter)
    
    p = subparsers.add_parser('create_chapter', help='Create a chapter')
    p.add_argument('--book-id', type=int, required=True)
    p.add_argument('--name', required=True)
    p.add_argument('--description')
    p.set_defaults(func=create_chapter)
    
    p = subparsers.add_parser('update_chapter', help='Update a chapter')
    p.add_argument('id', type=int)
    p.add_argument('--name')
    p.add_argument('--description')
    p.add_argument('--book-id', type=int)
    p.set_defaults(func=update_chapter)
    
    p = subparsers.add_parser('delete_chapter', help='Delete a chapter')
    p.add_argument('id', type=int)
    p.set_defaults(func=delete_chapter)
    
    # Pages
    p = subparsers.add_parser('list_pages', help='List all pages')
    p.add_argument('--count', type=int)
    p.set_defaults(func=list_pages)
    
    p = subparsers.add_parser('get_page', help='Get page with content')
    p.add_argument('id', type=int)
    p.add_argument('--content', action='store_true', help='Show full HTML')
    p.add_argument('--markdown', action='store_true', help='Show as markdown')
    p.set_defaults(func=get_page)
    
    p = subparsers.add_parser('create_page', help='Create a page')
    p.add_argument('--name', required=True)
    p.add_argument('--book-id', type=int)
    p.add_argument('--chapter-id', type=int)
    p.add_argument('--content', help='Content (auto-detect HTML/MD)')
    p.add_argument('--html', help='HTML content')
    p.add_argument('--markdown', help='Markdown content')
    p.set_defaults(func=create_page)
    
    p = subparsers.add_parser('update_page', help='Update a page')
    p.add_argument('id', type=int)
    p.add_argument('--name')
    p.add_argument('--content')
    p.add_argument('--html')
    p.add_argument('--markdown')
    p.add_argument('--book-id', type=int)
    p.add_argument('--chapter-id', type=int)
    p.set_defaults(func=update_page)
    
    p = subparsers.add_parser('delete_page', help='Delete a page')
    p.add_argument('id', type=int)
    p.set_defaults(func=delete_page)
    
    # Shelves
    p = subparsers.add_parser('list_shelves', help='List all shelves')
    p.add_argument('--count', type=int)
    p.set_defaults(func=list_shelves)
    
    p = subparsers.add_parser('get_shelf', help='Get shelf details')
    p.add_argument('id', type=int)
    p.set_defaults(func=get_shelf)
    
    p = subparsers.add_parser('create_shelf', help='Create a shelf')
    p.add_argument('name')
    p.add_argument('description', nargs='?')
    p.set_defaults(func=create_shelf)
    
    # Search
    p = subparsers.add_parser('search', help='Search content')
    p.add_argument('query')
    p.add_argument('--type', choices=['page', 'chapter', 'book', 'shelf'])
    p.add_argument('--count', type=int)
    p.set_defaults(func=search)
    
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        sys.exit(1)
    
    args.func(args)

if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)