From a5e15c802bb3ecdf4a2141d9e512c1653747ff97 Mon Sep 17 00:00:00 2001 From: Alexander Sidko Date: Tue, 18 Nov 2025 14:25:35 +0200 Subject: [PATCH 01/17] feat: add claude code cli detection and badge --- claude_code_detect.py | 74 ++++++++++++++++++++++++++++++++++++++++++ server.py | 16 +++++++-- templates/library.html | 15 +++++++-- templates/reader.html | 6 ++++ 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 claude_code_detect.py diff --git a/claude_code_detect.py b/claude_code_detect.py new file mode 100644 index 0000000..c4628a7 --- /dev/null +++ b/claude_code_detect.py @@ -0,0 +1,74 @@ +""" +Claude Code detection and authentication module. + +Detects if Claude Code is available and properly authenticated via +environment variables and CLI checks. +""" + +import os +import shutil +import subprocess + + +def is_claude_code_available() -> bool: + """Check if Claude Code CLI is installed and available in PATH.""" + return shutil.which("claude") is not None + + +def _has_env_auth() -> bool: + """Check for authentication via environment variables.""" + return bool( + os.getenv("ANTHROPIC_API_KEY") + or os.getenv("ANTHROPIC_AUTH_TOKEN") + or os.getenv("AWS_BEARER_TOKEN_BEDROCK") + ) + + +def _has_cli_auth() -> bool: + """Check Claude Code CLI auth by testing if it can run a command.""" + try: + result = subprocess.run( + ["claude", "-p", "test"], + capture_output=True, + text=True, + timeout=10 + ) + # Exit code 0 means command succeeded (authenticated) + # Non-zero or auth errors in stderr mean not authenticated + if result.returncode != 0: + return False + # Check stderr for auth-related errors + if result.stderr and ("auth" in result.stderr.lower() or "unauthorized" in result.stderr.lower()): + return False + return True + except subprocess.TimeoutExpired: + # If it times out, assume authenticated (Claude Code is processing) + return True + except Exception: + pass + return False + + +def is_authenticated() -> bool: + """Check if Claude Code is authenticated via environment or CLI.""" + return _has_env_auth() or _has_cli_auth() + + +def get_claude_code_status() -> dict: + """ + Get the current Claude Code status. + + Returns: + dict with keys: + - available: bool, whether Claude Code is installed + - authenticated: bool, whether Claude Code is authenticated + - enabled: bool, whether Claude Code is both available and authenticated + """ + available = is_claude_code_available() + authenticated = is_authenticated() if available else False + + return { + "available": available, + "authenticated": authenticated, + "enabled": available and authenticated, + } diff --git a/server.py b/server.py index 9c870dc..caba755 100644 --- a/server.py +++ b/server.py @@ -9,6 +9,7 @@ from fastapi.templating import Jinja2Templates from reader3 import Book, BookMetadata, ChapterContent, TOCEntry +from claude_code_detect import get_claude_code_status app = FastAPI() templates = Jinja2Templates(directory="templates") @@ -16,6 +17,9 @@ # Where are the book folders located? BOOKS_DIR = "." +# Get Claude Code status once at startup +CLAUDE_CODE_STATUS = get_claude_code_status() + @lru_cache(maxsize=10) def load_book_cached(folder_name: str) -> Optional[Book]: """ @@ -53,7 +57,14 @@ async def library_view(request: Request): "chapters": len(book.spine) }) - return templates.TemplateResponse("library.html", {"request": request, "books": books}) + return templates.TemplateResponse( + "library.html", + { + "request": request, + "books": books, + "claude_code_enabled": CLAUDE_CODE_STATUS["enabled"] + } + ) @app.get("/read/{book_id}", response_class=HTMLResponse) async def redirect_to_first_chapter(book_id: str): @@ -83,7 +94,8 @@ async def read_chapter(request: Request, book_id: str, chapter_index: int): "chapter_index": chapter_index, "book_id": book_id, "prev_idx": prev_idx, - "next_idx": next_idx + "next_idx": next_idx, + "claude_code_enabled": CLAUDE_CODE_STATUS["enabled"] }) @app.get("/read/{book_id}/images/{image_name}") diff --git a/templates/library.html b/templates/library.html index e7d094d..85b0332 100644 --- a/templates/library.html +++ b/templates/library.html @@ -7,7 +7,10 @@ @@ -43,6 +44,9 @@

Library

{{ book.author }}
{{ book.chapters }} sections + {% if book.summary %} +
{{ book.summary }}
+ {% endif %} Read Book {% endfor %} diff --git a/templates/reader.html b/templates/reader.html index 98b9783..a9f4023 100644 --- a/templates/reader.html +++ b/templates/reader.html @@ -103,6 +103,12 @@
+ {% if book_summary and chapter_index == 0 %} +
+
About this book
+
{{ book_summary }}
+
+ {% endif %}
{{ current_chapter.content | safe }}
From 51239fa322e4d1a2a08dbf803c4c502d3111c66d Mon Sep 17 00:00:00 2001 From: Alexander Sidko Date: Tue, 18 Nov 2025 14:52:35 +0200 Subject: [PATCH 03/17] feat: show book summary on every chapter with gray italic styling --- templates/reader.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/templates/reader.html b/templates/reader.html index a9f4023..aa95f70 100644 --- a/templates/reader.html +++ b/templates/reader.html @@ -103,10 +103,9 @@
- {% if book_summary and chapter_index == 0 %} -
-
About this book
-
{{ book_summary }}
+ {% if book_summary %} +
+ {{ book_summary }}
{% endif %}
From a7eebad365407f5caada9af1166a03855f109fbf Mon Sep 17 00:00:00 2001 From: Alexander Sidko Date: Tue, 18 Nov 2025 14:53:30 +0200 Subject: [PATCH 04/17] feat: add book context api and improve summary prompt quality --- book_info.py | 8 ++++++-- server.py | 23 +++++++++++++++++++++++ templates/reader.html | 12 +++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/book_info.py b/book_info.py index e102985..21b117c 100644 --- a/book_info.py +++ b/book_info.py @@ -70,11 +70,15 @@ def get_book_summary(book_id: str, title: str, author: str, content_sample: str) client = anthropic.Anthropic(api_key=api_key) message = client.messages.create( model="claude-haiku-4-5-20251001", - max_tokens=256, + max_tokens=300, messages=[ { "role": "user", - "content": f"""Summarize this book in one compact paragraph (2-3 sentences). + "content": f"""Write an engaging, intriguing one-paragraph summary of this book (3-4 sentences). The summary should: +- Hook the reader with what makes the book interesting +- Briefly describe the main plot/topic and protagonist +- Hint at the core conflict or central theme +- Be elegant and literary in tone Book: {title} by {author} diff --git a/server.py b/server.py index ff4530e..e5b6778 100644 --- a/server.py +++ b/server.py @@ -119,6 +119,29 @@ async def read_chapter(request: Request, book_id: str, chapter_index: int): "book_summary": summary }) +@app.get("/api/book/{book_id}/info") +async def get_book_info(book_id: str): + """Get book info (title, author, summary) for context.""" + book = load_book_cached(book_id) + if not book: + raise HTTPException(status_code=404, detail="Book not found") + + content_sample = book.spine[0].content[:1000] if book.spine else "" + summary = get_book_summary( + book_id, + book.metadata.title, + ", ".join(book.metadata.authors), + content_sample + ) + + return { + "title": book.metadata.title, + "author": ", ".join(book.metadata.authors), + "summary": summary, + "chapters": len(book.spine) + } + + @app.get("/read/{book_id}/images/{image_name}") async def serve_image(book_id: str, image_name: str): """ diff --git a/templates/reader.html b/templates/reader.html index aa95f70..44b5e45 100644 --- a/templates/reader.html +++ b/templates/reader.html @@ -38,7 +38,7 @@ - +