Strategy: TekDek foundational planning (personas, narrative, tools, Brick profile)
This commit is contained in:
477
skills/bookstack/scripts/bookstack.py
Normal file
477
skills/bookstack/scripts/bookstack.py
Normal file
@@ -0,0 +1,477 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user