From 25aaa981ef200acac27f100a1f20c4739437ca70 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 13:02:18 +0000 Subject: [PATCH 01/20] feat: make name field optional on Create and improve AI agent descriptions - Remove required: true from name field on Create operations for Page, Book, Chapter, and Shelf resources - Add generateFallbackName() method that auto-generates names from content (HTML/markdown headings for pages, description for others, timestamp as final fallback) - Update node description to communicate BookStack hierarchy (Shelves > Books > Chapters > Pages) - Enrich all operation action texts and field descriptions for better AI agent guidance via MCP (move semantics, search syntax, tag usage) - Update Global Search description with BookStack advanced filter syntax ({type:page}, {tag:name}, {in_name:text}, {in_body:text}) This enables AI agents to create and organize BookStack content without requiring manual name input, and provides clear guidance for token-efficient navigation of the content hierarchy. https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- nodes/Bookstack/Bookstack.node.ts | 39 ++++++++++++++++++- .../descriptions/Book.description.ts | 23 ++++++----- .../descriptions/Chapter.description.ts | 25 ++++++------ .../descriptions/Global.description.ts | 6 +-- .../descriptions/Page.description.ts | 27 +++++++------ .../descriptions/Shelf.description.ts | 23 ++++++----- 6 files changed, 88 insertions(+), 55 deletions(-) diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index 14a55a2..cdc4657 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -34,7 +34,7 @@ export class Bookstack implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Manage BookStack resources', + description: 'Manage BookStack content organized as Shelves > Books > Chapters > Pages. Use Search to find content, Get to see children, Create/Update to organize. Update can move content by changing parent IDs.', defaults: { name: 'Bookstack' }, inputs: ['main'], outputs: ['main'], @@ -111,6 +111,38 @@ export class Bookstack implements INodeType { return body; } + private generateFallbackName(resource: string, body: IDataObject): string { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + + if (resource === 'page') { + const html = body.html as string | undefined; + if (html) { + // Use [\s\S]*? to handle headings that may span multiple lines + const headingMatch = html.match(/]*>([\s\S]*?)<\/h[1-6]>/i); + if (headingMatch?.[1]) { + const text = headingMatch[1].replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); + if (text) return text.slice(0, 255); + } + const textContent = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); + if (textContent) return textContent.slice(0, 255); + } + const markdown = body.markdown as string | undefined; + if (markdown) { + const mdHeadingMatch = markdown.match(/^#{1,6}\s+(.+)$/m); + if (mdHeadingMatch?.[1]) return mdHeadingMatch[1].trim().slice(0, 255); + const firstLine = markdown.split(/\n/).find((l: string) => l.trim()); + if (firstLine) return firstLine.trim().slice(0, 255); + } + } + + if (['book', 'chapter', 'shelf'].includes(resource)) { + const desc = body.description as string | undefined; + if (desc?.trim()) return desc.trim().slice(0, 255); + } + + return `${resource}-${timestamp}`; + } + private validatePageCreation( body: IDataObject, context: IExecuteFunctions, @@ -397,6 +429,11 @@ export class Bookstack implements INodeType { this.validatePageCreation(body, context, itemIndex); } + // Auto-generate name if not provided (BookStack API requires it) + if (!body.name && this.resourceFields[resource]?.includes('name')) { + body.name = this.generateFallbackName(resource, body); + } + return await bookstackApiRequest.call(context, 'POST', `/${endpoint}`, body, {}); } diff --git a/nodes/Bookstack/descriptions/Book.description.ts b/nodes/Bookstack/descriptions/Book.description.ts index 12a8057..867df84 100644 --- a/nodes/Bookstack/descriptions/Book.description.ts +++ b/nodes/Bookstack/descriptions/Book.description.ts @@ -13,11 +13,11 @@ export const bookOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create a book (contains chapters and pages)' }, + { name: 'Delete', value: 'delete', action: 'Delete a book' }, + { name: 'Get', value: 'get', action: 'Get a book with its chapters and direct pages listed' }, + { name: 'Get Many', value: 'getAll', action: 'List books with filtering and sorting' }, + { name: 'Update', value: 'update', action: 'Update a book' }, ], default: 'getAll', }, @@ -36,14 +36,13 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'The unique identifier of the book', + description: 'Numeric ID of the book. Use Search or Get Many to find book IDs.', placeholder: 'Enter book ID (e.g., 123)', }, { displayName: 'Name', name: 'name', type: 'string', - required: true, displayOptions: { show: { resource: ['book'], @@ -51,7 +50,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the book (max 255 characters)', + description: 'Book title (max 255 chars). If omitted, auto-generated from description or timestamp.', }, { displayName: 'Name', @@ -64,7 +63,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the book (max 255 characters)', + description: 'New title for the book (max 255 chars). Leave empty to keep current name.', }, { displayName: 'Description', @@ -77,7 +76,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Description of the book (max 1900 characters)', + description: 'Short description of the book (max 1900 chars). Shown in listings and search results.', }, { displayName: 'Default Template ID', @@ -90,7 +89,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the default template for pages in this book', + description: 'ID of a page to use as the default template for new pages in this book.', }, { displayName: 'Tags', @@ -103,7 +102,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags for the book', + description: 'Comma-separated tags (e.g. "category:devops, status:active"). Tags enable search via {tag:name} syntax and are useful for categorization.', }, ...listOperations.map((op) => ({ ...op, diff --git a/nodes/Bookstack/descriptions/Chapter.description.ts b/nodes/Bookstack/descriptions/Chapter.description.ts index f3c0021..53da9d9 100644 --- a/nodes/Bookstack/descriptions/Chapter.description.ts +++ b/nodes/Bookstack/descriptions/Chapter.description.ts @@ -13,11 +13,11 @@ export const chapterOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create a chapter inside a book (groups related pages)' }, + { name: 'Delete', value: 'delete', action: 'Delete a chapter' }, + { name: 'Get', value: 'get', action: 'Get a chapter with its list of pages' }, + { name: 'Get Many', value: 'getAll', action: 'List chapters with filtering and sorting' }, + { name: 'Update', value: 'update', action: 'Update a chapter or move it to a different book' }, ], default: 'getAll', }, @@ -36,7 +36,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'The unique identifier of the chapter', + description: 'Numeric ID of the chapter. Use Search or Get Many to find chapter IDs.', placeholder: 'Enter chapter ID (e.g., 101)', }, { @@ -51,7 +51,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the book this chapter belongs to', + description: 'ID of the book this chapter belongs to.', placeholder: 'Enter book ID', }, { @@ -65,14 +65,13 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the book this chapter belongs to', + description: 'ID of the parent book. Set this to MOVE the chapter to a different book.', placeholder: 'Enter book ID', }, { displayName: 'Name', name: 'name', type: 'string', - required: true, displayOptions: { show: { resource: ['chapter'], @@ -80,7 +79,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the chapter (max 255 characters)', + description: 'Chapter title (max 255 chars). If omitted, auto-generated from description or timestamp.', placeholder: 'Enter chapter name', }, { @@ -94,7 +93,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the chapter (max 255 characters)', + description: 'New title for the chapter (max 255 chars). Leave empty to keep current name.', placeholder: 'Enter chapter name', }, { @@ -108,7 +107,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Description of the chapter (max 1900 characters)', + description: 'Short description of the chapter (max 1900 chars). Shown in listings and search results.', placeholder: 'Enter chapter description', }, { @@ -122,7 +121,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags for the chapter', + description: 'Comma-separated tags (e.g. "topic:networking, status:reviewed"). Tags enable search via {tag:name} syntax.', placeholder: 'tag1, tag2, tag3', }, ...listOperations.map((op) => ({ diff --git a/nodes/Bookstack/descriptions/Global.description.ts b/nodes/Bookstack/descriptions/Global.description.ts index 00a3b51..abacfc1 100644 --- a/nodes/Bookstack/descriptions/Global.description.ts +++ b/nodes/Bookstack/descriptions/Global.description.ts @@ -12,7 +12,7 @@ export const globalOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Search', value: 'search', action: 'Global search' }, + { name: 'Search', value: 'search', action: 'Search all content - use this first to find or locate content before creating or moving' }, { name: 'Audit Log', value: 'auditLogList', action: 'Audit log' }, ], default: 'search', @@ -38,7 +38,7 @@ export const globalFields: INodeProperties[] = [ }, placeholder: 'Enter search terms', description: - 'Search query to find content across all resources (books, pages, chapters, shelves)', + 'Search terms to find content across books, pages, chapters, and shelves. Supports advanced filters: {type:page}, {in_name:text}, {in_body:text}, {tag:tagname}, {tag:name=value}, {created_by:id}, {updated_by:id}. Use this as the first step to find where content belongs before creating or moving.', }, { displayName: 'Content Type Filter', @@ -105,7 +105,7 @@ export const globalFields: INodeProperties[] = [ }, default: false, description: - 'Whether to automatically retrieve full content for all found pages, chapters, books and shelves. This provides more context but may increase execution time.', + 'Automatically retrieve full content for all results. Provides complete text, children lists, and metadata. Enable this when you need to compare content or check for duplicates. Increases API calls proportionally to result count.', }, { displayName: 'Return All', diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index a13ad10..3f4de37 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -13,11 +13,11 @@ export const pageOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create a page in a book or chapter' }, + { name: 'Delete', value: 'delete', action: 'Delete a page' }, + { name: 'Get', value: 'get', action: 'Get a page with full HTML/markdown content' }, + { name: 'Get Many', value: 'getAll', action: 'List pages with filtering and sorting' }, + { name: 'Update', value: 'update', action: 'Update a page or move it to another book/chapter' }, ], default: 'getAll', }, @@ -36,14 +36,13 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'The unique identifier of the page', + description: 'Numeric ID of the page. Use Search or Get Many to find page IDs.', placeholder: 'Enter page ID (e.g., 456)', }, { displayName: 'Name', name: 'name', type: 'string', - required: true, displayOptions: { show: { resource: ['page'], @@ -51,7 +50,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the page (max 255 characters)', + description: 'Page title (max 255 chars). If omitted, auto-generated from the first heading or first line of content.', }, { displayName: 'Name', @@ -64,7 +63,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the page (max 255 characters)', + description: 'New title for the page (max 255 chars). Leave empty to keep current name.', }, { displayName: 'Book ID', @@ -77,7 +76,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the book this page belongs to (required on create if no Chapter ID)', + description: 'ID of the parent book. Required on create if chapter_id is not set. On update, set this to MOVE the page to a different book.', }, { displayName: 'Chapter ID', @@ -90,7 +89,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the chapter this page belongs to (required on create if no Book ID)', + description: 'ID of the parent chapter. Required on create if book_id is not set. On update, set this to MOVE the page into a different chapter.', }, { displayName: 'HTML Content', @@ -103,7 +102,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'HTML content of the page (required on create if no Markdown Content)', + description: 'HTML body of the page. Required on create if markdown is not set. On update, replaces existing content.', }, { displayName: 'Markdown Content', @@ -116,7 +115,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Markdown content of the page (required on create if no HTML Content)', + description: 'Markdown body of the page. Required on create if html is not set. On update, replaces existing content.', }, { displayName: 'Tags', @@ -129,7 +128,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags for the page', + description: 'Comma-separated tags (e.g. "topic:networking, status:reviewed"). Tags enable search via {tag:name} syntax and are useful for AI categorization.', }, ...listOperations.map((op) => ({ ...op, diff --git a/nodes/Bookstack/descriptions/Shelf.description.ts b/nodes/Bookstack/descriptions/Shelf.description.ts index 9a7ec2b..88c18ac 100644 --- a/nodes/Bookstack/descriptions/Shelf.description.ts +++ b/nodes/Bookstack/descriptions/Shelf.description.ts @@ -13,11 +13,11 @@ export const shelfOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create a shelf (top-level container for books)' }, + { name: 'Delete', value: 'delete', action: 'Delete a shelf' }, + { name: 'Get', value: 'get', action: 'Get a shelf with its list of books' }, + { name: 'Get Many', value: 'getAll', action: 'List shelves with filtering and sorting' }, + { name: 'Update', value: 'update', action: 'Update a shelf' }, ], default: 'getAll', }, @@ -36,14 +36,13 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'The unique identifier of the shelf', + description: 'Numeric ID of the shelf. Use Search or Get Many to find shelf IDs.', placeholder: 'Enter shelf ID (e.g., 789)', }, { displayName: 'Name', name: 'name', type: 'string', - required: true, displayOptions: { show: { resource: ['shelf'], @@ -51,7 +50,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the shelf (max 255 characters)', + description: 'Shelf title (max 255 chars). If omitted, auto-generated from description or timestamp.', placeholder: 'Enter shelf name', }, { @@ -65,7 +64,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the shelf (max 255 characters)', + description: 'New title for the shelf (max 255 chars). Leave empty to keep current name.', placeholder: 'Enter shelf name', }, { @@ -79,7 +78,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Description of the shelf (max 1900 characters)', + description: 'Short description of the shelf (max 1900 chars). Shown in listings and search results.', placeholder: 'Enter shelf description', }, { @@ -93,7 +92,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated list of book IDs to add to this shelf', + description: 'Comma-separated list of book IDs to assign to this shelf (e.g. "1,5,12"). Replaces the current book list on update.', placeholder: '123,456,789', }, { @@ -107,7 +106,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags for the shelf', + description: 'Comma-separated tags (e.g. "area:infrastructure, status:active"). Tags enable search via {tag:name} syntax.', placeholder: 'tag1, tag2, tag3', }, ...listOperations.map((op) => ({ From eb9e4b48f02e090acb68f238ca57b471c3ad644a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 13:07:41 +0000 Subject: [PATCH 02/20] chore: bump version to 1.4.0, update changelog, add CLAUDE.md - Bump version from 1.3.0 to 1.4.0 - Add v1.4.0 changelog entry documenting all new features - Add CLAUDE.md project knowledge base for AI-assisted development (covers architecture, API reference, workflow patterns, and pitfalls) https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- CHANGELOG.md | 12 ++ CLAUDE.md | 313 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 CLAUDE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4862caf..b913911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.4.0](https://github.com/lucaguindani/n8n-nodes-bookstack/compare/1.3.0...1.4.0) + +> 8 April 2026 + +- Make `name` field optional on Create operations for Page, Book, Chapter, and Shelf resources +- Add auto-generation of names from content when not provided (extracts headings from HTML/markdown for pages, uses description for other resources, falls back to timestamp) +- Improve all operation action texts for better AI agent guidance via MCP (e.g. "Update a page or move it to another book/chapter") +- Enrich field descriptions with move semantics, search syntax hints, and tag usage guidance +- Update Global Search description with BookStack advanced filter syntax (`{type:page}`, `{tag:name}`, `{in_name:text}`, `{in_body:text}`) +- Update node-level description to communicate content hierarchy (Shelves > Books > Chapters > Pages) +- Add CLAUDE.md project knowledge base for AI-assisted development + #### [1.3.0](https://github.com/lucaguindani/n8n-nodes-bookstack/compare/1.2.0...1.3.0) - Bump flatted in the npm_and_yarn group across 1 directory [`#6`](https://github.com/lucaguindani/n8n-nodes-bookstack/pull/6) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e6a0b81 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,313 @@ +# CLAUDE.md - Project Knowledge Base for AI-Assisted Development + +This file contains essential context for LLMs working on this project. +Read this first before making any changes. + +--- + +## Project Overview + +**n8n-nodes-bookstack** is a community n8n node that integrates with the BookStack API. +It provides CRUD operations for BookStack's content hierarchy and is designed to be +used both manually in n8n workflows and by AI agents via MCP (`usableAsTool: true`). + +- **Version**: 1.4.0 +- **License**: MIT +- **Runtime**: Node 20+, n8n 1.109+, BookStack 24.5+ +- **Package Manager**: pnpm + +--- + +## BookStack Content Hierarchy + +``` +Shelf (top-level container) + └── Book (contains chapters and/or direct pages) + ├── Chapter (groups related pages within a book) + │ └── Page (content unit with HTML or Markdown body) + └── Page (direct page without chapter) +``` + +- A **Shelf** holds multiple **Books** (many-to-many via book IDs) +- A **Book** contains **Chapters** and/or direct **Pages** +- A **Chapter** belongs to exactly one Book and contains **Pages** +- A **Page** belongs to either a Book or a Chapter (not both simultaneously) +- **Tags** can be applied to Books, Chapters, Pages, and Shelves + +--- + +## Architecture & File Structure + +``` +credentials/ + BookstackApi.credentials.ts # API auth (Token ID + Token Secret) + +nodes/Bookstack/ + Bookstack.node.ts # Main node class (all operation handlers) + Bookstack.node.json # Node metadata for n8n registry + descriptions/ + ResourceProperty.ts # Resource selector (dropdown) + Book.description.ts # Book CRUD field definitions + Chapter.description.ts # Chapter CRUD field definitions + Page.description.ts # Page CRUD field definitions + Shelf.description.ts # Shelf CRUD field definitions + Attachment.description.ts # Attachment CRUD (file/link) + Image.description.ts # Image gallery CRUD + Global.description.ts # Global Search + Audit Log + ListOperations.ts # Shared filtering/sorting/pagination + types/ + BookstackTypes.ts # Filter interface + utils/ + BookstackApiHelpers.ts # HTTP request functions + validation +``` + +--- + +## Key Design Patterns + +### 1. Node Class Structure (`Bookstack.node.ts`) + +The `Bookstack` class implements `INodeType` with: + +- **`resourceEndpoints`**: Maps resource names to API paths + - `book` -> `books`, `page` -> `pages`, `chapter` -> `chapters`, + `shelf` -> `shelves`, `attachment` -> `attachments`, `image` -> `image-gallery` + +- **`resourceFields`**: Lists writable fields per resource + - `book`: name, description, tags, default_template_id + - `page`: name, html, markdown, book_id, chapter_id, tags + - `chapter`: name, description, book_id, tags + - `shelf`: name, description, books, tags + +- **`buildRequestBody()`**: Reads all fields for a resource, skips undefined/empty values. + Converts tags from comma-separated string to `[{name: "tag"}]` array format. + Converts shelf books from comma-separated string to integer array. + +- **`generateFallbackName()`**: Auto-generates a name when not provided: + - Pages: First HTML heading (h1-h6) -> first text content -> first markdown heading -> first line -> timestamp + - Books/Chapters/Shelves: Description text (truncated to 255) -> timestamp + - Fallback format: `{resource}-{ISO-timestamp}` + +- **`validatePageCreation()`**: Ensures pages have either book_id or chapter_id, and either html or markdown. + +### 2. Operation Flow + +``` +execute() -> for each item: + 1. Read resource + operation from parameters + 2. Route to handler: + - book/page/shelf/chapter: handleCreateOperation / handleGetOperation / etc. + - attachment: handleCreateAttachmentOperation (multipart) + - image: handleCreateImageOperation (multipart) + - global: handleSearchOperation / handleAuditLogOperation + 3. Handler builds request body, validates, calls API + 4. Response mapped to INodeExecutionData +``` + +### 3. Description Files Pattern + +Each resource has two exports: +- `{resource}Operations`: Operation dropdown (Create/Delete/Get/GetMany/Update) +- `{resource}Fields`: Parameter definitions with `displayOptions` controlling visibility + +**Important for AI/MCP**: The `action` string on each operation option becomes the tool +action description. The `description` string on each field is what AI agents read to +understand parameters. Both should be clear and actionable. + +### 4. Optional vs Required Fields + +- Fields with `required: true` become mandatory MCP tool parameters (AI must provide them) +- Fields without `required` are optional - AI can skip them +- The `default: ''` value is for UI initialization only; `getNodeParameter()` with + fallback `undefined` returns `undefined` when the user/AI doesn't provide a value +- `buildRequestBody()` skips fields where `value === undefined || value === ''` + +### 5. Auto-Name Generation (v1.4.0) + +The `name` field is optional on Create operations. The BookStack API requires it, +so `handleCreateOperation()` calls `generateFallbackName()` when `body.name` is falsy. +This allows AI agents to either: +- Generate their own name (guided by the field description) +- Omit the name entirely (fallback auto-generation kicks in) + +--- + +## BookStack API Reference + +### Authentication +``` +Authorization: Token {tokenId}:{tokenSecret} +``` +Base URL includes `/api` path (e.g., `https://bookstack.example.com/api`). + +### Core Endpoints + +| Method | Endpoint | Notes | +|--------|----------|-------| +| GET | /books | List books (supports count, offset, sort, filter) | +| GET | /books/{id} | Returns book with `contents` array (chapters + direct pages) | +| POST | /books | Create book. Required: `name` | +| PUT | /books/{id} | Update book | +| DELETE | /books/{id} | Delete book | +| GET | /chapters | List chapters | +| GET | /chapters/{id} | Returns chapter with `pages` array | +| POST | /chapters | Create chapter. Required: `name`, `book_id` | +| PUT | /chapters/{id} | Update chapter. Change `book_id` to MOVE chapter | +| DELETE | /chapters/{id} | Delete chapter | +| GET | /pages | List pages | +| GET | /pages/{id} | Returns page with full html/markdown content | +| POST | /pages | Create page. Required: `name`, `book_id` OR `chapter_id`, `html` OR `markdown` | +| PUT | /pages/{id} | Update page. Change `book_id`/`chapter_id` to MOVE page | +| DELETE | /pages/{id} | Delete page | +| GET | /shelves | List shelves | +| GET | /shelves/{id} | Returns shelf with `books` array | +| POST | /shelves | Create shelf. Required: `name` | +| PUT | /shelves/{id} | Update shelf | +| DELETE | /shelves/{id} | Delete shelf | +| GET | /search | Full-text search. Query param: `query` | +| GET | /audit-log | Activity log (requires admin permissions) | +| GET/POST/PUT/DELETE | /attachments/* | File/link attachments on pages | +| GET/POST/PUT/DELETE | /image-gallery/* | Image uploads for pages | + +### Search Query Syntax + +BookStack search supports inline filters appended to the query string: +- `{type:page}` / `{type:book}` / `{type:chapter}` / `{type:bookshelf}` - filter by content type +- `{in_name:text}` - search only in names/titles +- `{in_body:text}` - search only in body content +- `{tag:tagname}` - filter by tag name +- `{tag:name=value}` - filter by tag name-value pair +- `{created_by:user_id}` - filter by creator +- `{updated_by:user_id}` - filter by last editor +- `{is_restricted}` - only restricted content +- `{viewed_by_me}` / `{not_viewed_by_me}` - personal view history + +### Pagination + +All list endpoints support: +- `count`: Items per page (max 500) +- `offset`: Skip N items +- `sort`: Field name prefixed with `+` (asc) or `-` (desc) +- `filter[field:op]`: Field-level filtering (eq, ne, gt, gte, lt, lte, like) + +### Field Constraints + +- `name`: max 255 characters (all resources) +- `description`: max 1900 characters (books, chapters, shelves) +- `tags`: Array of `{name: string, value?: string}` objects +- `books` (shelf): Array of integer book IDs + +### API Endpoints NOT Implemented in This Node + +These BookStack API endpoints exist but are not yet in the node: +- `GET /pages/{id}/export/html` - Export page as HTML +- `GET /pages/{id}/export/markdown` - Export page as Markdown +- `GET /pages/{id}/export/plaintext` - Export page as plain text +- `GET /books/{id}/export/html` - Export book as HTML +- `GET /books/{id}/export/pdf` - Export book as PDF +- `GET /chapters/{id}/export/html` - Export chapter as HTML +- Content comments API (if available in BookStack version) +- User/Role management endpoints +- Recycle bin endpoints +- Webhook management endpoints + +--- + +## AI Agent Workflow (Intended Use Case) + +The primary AI use case is automated content organization: + +1. **Webhook triggers** when new content is created in BookStack (e.g., a note dropped into an "inbox" shelf) +2. **AI agent reads** the new content via Get Page +3. **AI searches** for where content belongs using Global Search with type filters +4. **AI navigates** the hierarchy efficiently: Get Shelf -> Get Book -> Get Chapter +5. **AI decides** to: + - Move the page (Update Page with new `chapter_id` or `book_id`) + - Merge with existing content (Update existing page, delete duplicate) + - Create new structure (Create Chapter/Book if needed) + - Rename the page (Update with new `name`) + - Tag for categorization (Update with `tags`) + +### Token-Efficient Navigation Strategy + +To avoid loading the entire BookStack: +1. **Search first** (Global Search with type filter) - returns minimal data (id, name, type, preview) +2. **Get specific parent** (Get Book/Chapter) - returns children list (one level deep) +3. **Get specific page** only when needed for content comparison +4. **Use tags** for categorization - enables `{tag:name}` search without scanning all content +5. **Avoid "Deep Dive"** unless comparing content for duplicates (makes N+1 API calls) + +--- + +## Development Commands + +```bash +pnpm install # Install dependencies +pnpm run build # Build TypeScript to dist/ +pnpm run dev # Start local n8n instance with this node +pnpm run lint # Run ESLint +pnpm run format # Check formatting with Prettier +pnpm run lint:fix # Auto-fix lint issues +pnpm run format:fix # Auto-fix formatting +``` + +### Build Output + +`pnpm build` runs `n8n-node build` which: +1. Compiles TypeScript from `nodes/` and `credentials/` to `dist/` +2. Copies static files (icons, JSON metadata) + +The `dist/` directory is what n8n loads at runtime. + +--- + +## Testing Notes + +- No automated test suite exists yet +- Test manually by running `pnpm run dev` and using the node in n8n UI +- Key scenarios to test after changes: + - Create Page/Book/Chapter/Shelf with name (should work as before) + - Create Page/Book/Chapter/Shelf without name (should auto-generate) + - Update Page with different `book_id`/`chapter_id` (should move page) + - Global Search with various filters + - Deep Dive on search results + +--- + +## Common Pitfalls + +1. **`getNodeParameter` fallback**: Always pass `undefined` as third arg for optional fields, + not `''`. Empty string would be treated as a provided value. + +2. **Tags format**: UI accepts comma-separated strings, but BookStack API expects + `[{name: "tag"}]`. The `buildRequestBody()` method handles conversion automatically. + +3. **Shelf books format**: UI accepts comma-separated IDs (e.g., "1,5,12"), converted + to `[1, 5, 12]` integer array by `buildRequestBody()`. + +4. **Attachment/Image creation**: These bypass `buildRequestBody()` and `handleCreateOperation()`. + They use dedicated multipart handlers. Changes to the shared create flow do NOT affect them. + +5. **Page requires content**: Unlike books/chapters/shelves, pages MUST have either + `html` or `markdown` on creation. This is enforced by `validatePageCreation()`. + +6. **Moving content**: Pages are moved by setting `book_id` or `chapter_id` on Update. + Chapters are moved by setting `book_id` on Update. Books cannot be moved (they're top-level). + Shelves reference books via the `books` ID array (many-to-many). + +7. **Search type filter**: The node appends `{type:X}` to the search query string. + BookStack type values: `page`, `chapter`, `book`, `bookshelf` (note: "bookshelf", not "shelf"). + +--- + +## Version History + +| Version | Key Changes | +|---------|-------------| +| 1.4.0 | Optional name on Create, auto-name generation, AI-optimized descriptions, CLAUDE.md | +| 1.3.0 | Attachment resource support with CRUD and multipart handling | +| 1.2.0 | Image resource support with CRUD and multipart handling | +| 1.1.0 | Switch to pnpm | +| 1.0.0 | Deep Dive in main node, "Return All" pagination, unified search | +| 0.11.x | Audit log, simplified API, multi-Node version matrix | +| 0.10.x | Initial release, BookStack CRUD for books/pages/chapters/shelves | diff --git a/package.json b/package.json index ff17dc8..c86e139 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-bookstack", - "version": "1.3.0", + "version": "1.4.0", "description": "Community n8n node for the BookStack API", "keywords": [ "n8n", From 30e3e52ab72e9ef72c76088dc79189616da71027 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 13:15:50 +0000 Subject: [PATCH 03/20] fix: QA improvements - performance, entity decoding, markdown-first strategy Addresses issues found in QA regression audit: - Limit HTML text extraction to first 2000 chars to avoid expensive regex on large documents (performance fix) - Add decodeHtmlEntities() to prevent names like " " or "&" from auto-generated page titles - Reorder name extraction to try Markdown BEFORE HTML (markdown is the preferred format for AI workflows) - Update page field descriptions to clearly recommend Markdown over HTML (~3x fewer tokens, more AI-efficient) - Add "Do NOT set both html and markdown" guidance to prevent undefined behavior - Update CLAUDE.md with Markdown-first policy and pitfall notes https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- CLAUDE.md | 13 ++++++ nodes/Bookstack/Bookstack.node.ts | 41 ++++++++++++++----- .../descriptions/Page.description.ts | 6 +-- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e6a0b81..c7d2d6b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,6 +190,18 @@ All list endpoints support: - `sort`: Field name prefixed with `+` (asc) or `-` (desc) - `filter[field:op]`: Field-level filtering (eq, ne, gt, gte, lt, lte, like) +### Markdown vs HTML (Important for AI Usage) + +**Always prefer Markdown over HTML for page content.** Reasons: +- Markdown uses ~3x fewer tokens than equivalent HTML +- AI agents process Markdown more efficiently (less noise from tags) +- BookStack stores both formats but only one should be set per request +- **Do NOT set both `html` and `markdown`** in the same create/update call - BookStack + will use one and ignore the other, behavior is undefined + +The `html` field exists as a fallback for users who need precise formatting control +or cannot use Markdown. For AI-driven workflows, Markdown is the gold standard. + ### Field Constraints - `name`: max 255 characters (all resources) @@ -290,6 +302,7 @@ The `dist/` directory is what n8n loads at runtime. 5. **Page requires content**: Unlike books/chapters/shelves, pages MUST have either `html` or `markdown` on creation. This is enforced by `validatePageCreation()`. + Always use `markdown` unless the user explicitly needs HTML. Never set both fields. 6. **Moving content**: Pages are moved by setting `book_id` or `chapter_id` on Update. Chapters are moved by setting `book_id` on Update. Books cannot be moved (they're top-level). diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index cdc4657..75340b5 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -111,21 +111,23 @@ export class Bookstack implements INodeType { return body; } + private static decodeHtmlEntities(text: string): string { + return text + .replace(/ /gi, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code))); + } + private generateFallbackName(resource: string, body: IDataObject): string { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); if (resource === 'page') { - const html = body.html as string | undefined; - if (html) { - // Use [\s\S]*? to handle headings that may span multiple lines - const headingMatch = html.match(/]*>([\s\S]*?)<\/h[1-6]>/i); - if (headingMatch?.[1]) { - const text = headingMatch[1].replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); - if (text) return text.slice(0, 255); - } - const textContent = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); - if (textContent) return textContent.slice(0, 255); - } + // Prefer markdown (token-efficient, AI-friendly) over HTML for name extraction const markdown = body.markdown as string | undefined; if (markdown) { const mdHeadingMatch = markdown.match(/^#{1,6}\s+(.+)$/m); @@ -133,6 +135,23 @@ export class Bookstack implements INodeType { const firstLine = markdown.split(/\n/).find((l: string) => l.trim()); if (firstLine) return firstLine.trim().slice(0, 255); } + + // HTML fallback: only process a prefix to avoid expensive regex on large documents + const html = body.html as string | undefined; + if (html) { + const htmlPrefix = html.slice(0, 2000); + const headingMatch = htmlPrefix.match(/]*>([\s\S]*?)<\/h[1-6]>/i); + if (headingMatch?.[1]) { + const text = Bookstack.decodeHtmlEntities( + headingMatch[1].replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(), + ); + if (text) return text.slice(0, 255); + } + const textContent = Bookstack.decodeHtmlEntities( + htmlPrefix.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(), + ); + if (textContent) return textContent.slice(0, 255); + } } if (['book', 'chapter', 'shelf'].includes(resource)) { diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index 3f4de37..b5a1748 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -13,7 +13,7 @@ export const pageOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create a page in a book or chapter' }, + { name: 'Create', value: 'create', action: 'Create a page in a book or chapter (prefer markdown over html)' }, { name: 'Delete', value: 'delete', action: 'Delete a page' }, { name: 'Get', value: 'get', action: 'Get a page with full HTML/markdown content' }, { name: 'Get Many', value: 'getAll', action: 'List pages with filtering and sorting' }, @@ -102,7 +102,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'HTML body of the page. Required on create if markdown is not set. On update, replaces existing content.', + description: 'HTML body of the page. Only use this if markdown is not suitable. Prefer markdown over HTML - it uses fewer tokens and is more efficient for AI processing. Required on create if markdown is not set. Do NOT set both html and markdown.', }, { displayName: 'Markdown Content', @@ -115,7 +115,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Markdown body of the page. Required on create if html is not set. On update, replaces existing content.', + description: 'Markdown body of the page. PREFERRED over HTML - uses ~3x fewer tokens and is more efficient for AI processing. Required on create if html is not set. Do NOT set both html and markdown.', }, { displayName: 'Tags', From 57d14bed15786380e9285f8a190df19bc234454e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 13:22:39 +0000 Subject: [PATCH 04/20] feat: encode token-efficient navigation strategy in all tool descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevents AI agents from accidentally loading entire BookStack instances (which could cost millions of tokens on large instances). Changes across all description files: - Node description: encodes Search → Get by ID → Update/Create strategy - Search action: explicitly states it returns only IDs and previews - Search query: documents 3-step workflow (search → get → update) - Get Many actions: WARNING to use Search instead on large instances - Get (single) actions: positioned as follow-up step after Search - Return All: WARNING about catastrophic token usage on large datasets - Limit: guidance to keep low (10-20) and use Search instead - Deep Dive: WARNING about N+1 API calls, recommend small limits - ListOperations: Return All and Limit warnings for shared use - CLAUDE.md: token cost comparison table and ALWAYS/NEVER rules https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- CLAUDE.md | 31 +++++++++++++++---- nodes/Bookstack/Bookstack.node.ts | 2 +- .../descriptions/Book.description.ts | 4 +-- .../descriptions/Chapter.description.ts | 4 +-- .../descriptions/Global.description.ts | 10 +++--- .../Bookstack/descriptions/ListOperations.ts | 4 +-- .../descriptions/Page.description.ts | 4 +-- .../descriptions/Shelf.description.ts | 4 +-- 8 files changed, 41 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c7d2d6b..1039aed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -242,12 +242,31 @@ The primary AI use case is automated content organization: ### Token-Efficient Navigation Strategy -To avoid loading the entire BookStack: -1. **Search first** (Global Search with type filter) - returns minimal data (id, name, type, preview) -2. **Get specific parent** (Get Book/Chapter) - returns children list (one level deep) -3. **Get specific page** only when needed for content comparison -4. **Use tags** for categorization - enables `{tag:name}` search without scanning all content -5. **Avoid "Deep Dive"** unless comparing content for duplicates (makes N+1 API calls) +A BookStack with 1000 books and 100,000 pages would cost millions of tokens if loaded +entirely. The tool descriptions encode the following strategy to prevent this: + +**ALWAYS DO:** +1. **Search first** (Global Search with type filter, limit 5-20) - returns only IDs, names, + and short previews (~50 tokens per result vs ~5000 tokens for full page content) +2. **Get single items by ID** - after Search identifies candidates, fetch only the specific + pages/books/chapters you need to inspect +3. **Use tags** for categorization - enables `{tag:name}` search without scanning content +4. **Navigate top-down** when needed: Get Shelf (lists books) -> Get Book (lists chapters + and pages) -> Get Chapter (lists pages) -> Get Page (full content) + +**NEVER DO:** +- `Get Many Pages` with `Return All = true` - loads ALL pages from the instance +- `Get Many` with high limits (>50) when you only need a few specific items +- `Deep Dive` with `Return All` or high limits - multiplies API calls (N+1 pattern) +- Fetch full content of items you don't need to read + +**Token cost comparison (approximate):** +| Operation | Tokens per item | 1000 items | +|-----------|----------------|------------| +| Search result (preview) | ~50 | ~50,000 | +| Get Many result (no content) | ~100 | ~100,000 | +| Get single page (full content) | ~2,000-50,000 | CATASTROPHIC | +| Deep Dive search result | ~2,000-50,000 | CATASTROPHIC | --- diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index 75340b5..85cea78 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -34,7 +34,7 @@ export class Bookstack implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Manage BookStack content organized as Shelves > Books > Chapters > Pages. Use Search to find content, Get to see children, Create/Update to organize. Update can move content by changing parent IDs.', + description: 'Manage BookStack content (Shelves > Books > Chapters > Pages). IMPORTANT: To find content, always use Search first (returns only IDs and previews), then Get single items by ID. NEVER use Get Many with Return All on large instances - it loads everything and wastes massive tokens. Strategy: Search → Get by ID → Update/Create.', defaults: { name: 'Bookstack' }, inputs: ['main'], outputs: ['main'], diff --git a/nodes/Bookstack/descriptions/Book.description.ts b/nodes/Bookstack/descriptions/Book.description.ts index 867df84..dccda0c 100644 --- a/nodes/Bookstack/descriptions/Book.description.ts +++ b/nodes/Bookstack/descriptions/Book.description.ts @@ -15,8 +15,8 @@ export const bookOperations: INodeProperties[] = [ options: [ { name: 'Create', value: 'create', action: 'Create a book (contains chapters and pages)' }, { name: 'Delete', value: 'delete', action: 'Delete a book' }, - { name: 'Get', value: 'get', action: 'Get a book with its chapters and direct pages listed' }, - { name: 'Get Many', value: 'getAll', action: 'List books with filtering and sorting' }, + { name: 'Get', value: 'get', action: 'Get a single book by ID with its table of contents (chapters and pages listed, not full content)' }, + { name: 'Get Many', value: 'getAll', action: 'List books (prefer Search to find books by keyword instead)' }, { name: 'Update', value: 'update', action: 'Update a book' }, ], default: 'getAll', diff --git a/nodes/Bookstack/descriptions/Chapter.description.ts b/nodes/Bookstack/descriptions/Chapter.description.ts index 53da9d9..beaf157 100644 --- a/nodes/Bookstack/descriptions/Chapter.description.ts +++ b/nodes/Bookstack/descriptions/Chapter.description.ts @@ -15,8 +15,8 @@ export const chapterOperations: INodeProperties[] = [ options: [ { name: 'Create', value: 'create', action: 'Create a chapter inside a book (groups related pages)' }, { name: 'Delete', value: 'delete', action: 'Delete a chapter' }, - { name: 'Get', value: 'get', action: 'Get a chapter with its list of pages' }, - { name: 'Get Many', value: 'getAll', action: 'List chapters with filtering and sorting' }, + { name: 'Get', value: 'get', action: 'Get a single chapter by ID with its list of pages (use after Search)' }, + { name: 'Get Many', value: 'getAll', action: 'List chapters (prefer Search to find chapters by keyword instead)' }, { name: 'Update', value: 'update', action: 'Update a chapter or move it to a different book' }, ], default: 'getAll', diff --git a/nodes/Bookstack/descriptions/Global.description.ts b/nodes/Bookstack/descriptions/Global.description.ts index abacfc1..3fc6d72 100644 --- a/nodes/Bookstack/descriptions/Global.description.ts +++ b/nodes/Bookstack/descriptions/Global.description.ts @@ -12,7 +12,7 @@ export const globalOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Search', value: 'search', action: 'Search all content - use this first to find or locate content before creating or moving' }, + { name: 'Search', value: 'search', action: 'Search content by keywords - ALWAYS use this first instead of Get Many (returns only IDs, names, and previews - very token-efficient)' }, { name: 'Audit Log', value: 'auditLogList', action: 'Audit log' }, ], default: 'search', @@ -38,7 +38,7 @@ export const globalFields: INodeProperties[] = [ }, placeholder: 'Enter search terms', description: - 'Search terms to find content across books, pages, chapters, and shelves. Supports advanced filters: {type:page}, {in_name:text}, {in_body:text}, {tag:tagname}, {tag:name=value}, {created_by:id}, {updated_by:id}. Use this as the first step to find where content belongs before creating or moving.', + 'Search terms to find content. Returns only IDs, names, and short previews (token-efficient). Supports filters: {type:page}, {in_name:text}, {in_body:text}, {tag:tagname}, {tag:name=value}, {created_by:id}, {updated_by:id}. Workflow: 1) Search here to find candidates, 2) Get individual items by ID to read full content, 3) Update or Create as needed. NEVER use Get Many to browse - always search first.', }, { displayName: 'Content Type Filter', @@ -72,7 +72,7 @@ export const globalFields: INodeProperties[] = [ }, }, default: false, - description: 'Whether to return all results or only up to a given limit', + description: 'WARNING: On large BookStack instances this can return thousands of results. Use a low limit instead and refine your search query. Only enable if you are certain the result set is small.', }, { displayName: 'Limit', @@ -90,7 +90,7 @@ export const globalFields: INodeProperties[] = [ minValue: 1, maxValue: 100, }, - description: 'Max number of results to return', + description: 'Max results to return. Keep this low (5-20) when searching to save tokens. You can always search again with different terms if needed.', placeholder: '100', }, { @@ -105,7 +105,7 @@ export const globalFields: INodeProperties[] = [ }, default: false, description: - 'Automatically retrieve full content for all results. Provides complete text, children lists, and metadata. Enable this when you need to compare content or check for duplicates. Increases API calls proportionally to result count.', + 'WARNING: Fetches FULL content for every search result (1 extra API call per result). With 50 results this means 50 additional requests and massive token usage. Only use with a small limit (5-10) when you need to compare content. Prefer: search with low limit, then Get individual pages by ID.', }, { displayName: 'Return All', diff --git a/nodes/Bookstack/descriptions/ListOperations.ts b/nodes/Bookstack/descriptions/ListOperations.ts index 25266ab..f6cfc77 100644 --- a/nodes/Bookstack/descriptions/ListOperations.ts +++ b/nodes/Bookstack/descriptions/ListOperations.ts @@ -78,7 +78,7 @@ export const listOperations: INodeProperties[] = [ name: 'returnAll', type: 'boolean', default: false, - description: 'Whether to return all results or only up to a given limit', + description: 'WARNING: Fetches ALL items across all pages. On large BookStack instances with thousands of items this wastes massive amounts of tokens. Use Search (Global resource) to find specific items instead. Only enable for small, bounded datasets.', }, { displayName: 'Limit', @@ -88,7 +88,7 @@ export const listOperations: INodeProperties[] = [ minValue: 1, }, default: 50, - description: 'Max number of results to return', + description: 'Max items to return. Keep low (10-20) to save tokens. Use Search (Global resource) with keywords to find specific items instead of listing large numbers.', displayOptions: { show: { returnAll: [false], diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index b5a1748..42f7fae 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -15,8 +15,8 @@ export const pageOperations: INodeProperties[] = [ options: [ { name: 'Create', value: 'create', action: 'Create a page in a book or chapter (prefer markdown over html)' }, { name: 'Delete', value: 'delete', action: 'Delete a page' }, - { name: 'Get', value: 'get', action: 'Get a page with full HTML/markdown content' }, - { name: 'Get Many', value: 'getAll', action: 'List pages with filtering and sorting' }, + { name: 'Get', value: 'get', action: 'Get a single page by ID with full content (use after Search to read specific pages)' }, + { name: 'Get Many', value: 'getAll', action: 'List pages (WARNING: use Search instead to find pages - Get Many on large instances wastes tokens)' }, { name: 'Update', value: 'update', action: 'Update a page or move it to another book/chapter' }, ], default: 'getAll', diff --git a/nodes/Bookstack/descriptions/Shelf.description.ts b/nodes/Bookstack/descriptions/Shelf.description.ts index 88c18ac..cd35b39 100644 --- a/nodes/Bookstack/descriptions/Shelf.description.ts +++ b/nodes/Bookstack/descriptions/Shelf.description.ts @@ -15,8 +15,8 @@ export const shelfOperations: INodeProperties[] = [ options: [ { name: 'Create', value: 'create', action: 'Create a shelf (top-level container for books)' }, { name: 'Delete', value: 'delete', action: 'Delete a shelf' }, - { name: 'Get', value: 'get', action: 'Get a shelf with its list of books' }, - { name: 'Get Many', value: 'getAll', action: 'List shelves with filtering and sorting' }, + { name: 'Get', value: 'get', action: 'Get a single shelf by ID with its list of books (use after Search)' }, + { name: 'Get Many', value: 'getAll', action: 'List shelves (prefer Search to find shelves by keyword instead)' }, { name: 'Update', value: 'update', action: 'Update a shelf' }, ], default: 'getAll', From 3b3b4b61110c35b2bbc3b8caa8527d72f41e3969 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 13:41:04 +0000 Subject: [PATCH 05/20] fix: guard against whitespace-only markdown headings in fallback name generation Add .trim() check before returning extracted heading or first line from markdown content. Prevents empty string being sent as page name to BookStack API (which requires a non-empty name). Found during final QA dry check. https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- nodes/Bookstack/Bookstack.node.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index 85cea78..aba977d 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -131,9 +131,9 @@ export class Bookstack implements INodeType { const markdown = body.markdown as string | undefined; if (markdown) { const mdHeadingMatch = markdown.match(/^#{1,6}\s+(.+)$/m); - if (mdHeadingMatch?.[1]) return mdHeadingMatch[1].trim().slice(0, 255); + if (mdHeadingMatch?.[1]?.trim()) return mdHeadingMatch[1].trim().slice(0, 255); const firstLine = markdown.split(/\n/).find((l: string) => l.trim()); - if (firstLine) return firstLine.trim().slice(0, 255); + if (firstLine?.trim()) return firstLine.trim().slice(0, 255); } // HTML fallback: only process a prefix to avoid expensive regex on large documents From 84722b82e7f3b929bcaf129425b749373b9ba14f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 13:46:16 +0000 Subject: [PATCH 06/20] docs: add KNOWN_ISSUES.md, update changelog and learnings - Create KNOWN_ISSUES.md documenting 16 pre-existing issues found during QA audit (4 MEDIUM, 8 LOW, 4 INFO - none from v1.4.0) - Update CHANGELOG.md with complete v1.4.0 feature and QA sections - Update CLAUDE.md with QA learnings: known issues summary, new pitfalls (new Bookstack() pattern, audit-log sort, decodeHtmlEntities limitations, NodeApiError detection), updated version history https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- CHANGELOG.md | 15 +++- CLAUDE.md | 37 +++++++- KNOWN_ISSUES.md | 220 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 KNOWN_ISSUES.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b913911..ef28608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,22 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). > 8 April 2026 +##### Features - Make `name` field optional on Create operations for Page, Book, Chapter, and Shelf resources -- Add auto-generation of names from content when not provided (extracts headings from HTML/markdown for pages, uses description for other resources, falls back to timestamp) -- Improve all operation action texts for better AI agent guidance via MCP (e.g. "Update a page or move it to another book/chapter") +- Add `generateFallbackName()` for auto-generation of names from content (extracts headings from HTML/markdown for pages, uses description for other resources, falls back to timestamp) +- Add `decodeHtmlEntities()` for clean auto-generated names from HTML content +- Improve all operation action texts for better AI agent guidance via MCP - Enrich field descriptions with move semantics, search syntax hints, and tag usage guidance - Update Global Search description with BookStack advanced filter syntax (`{type:page}`, `{tag:name}`, `{in_name:text}`, `{in_body:text}`) +- Encode token-efficient navigation strategy in all tool descriptions (Search first, Get by ID, never Get Many with Return All on large instances) +- Add warnings to Return All, Deep Dive, and Get Many operations about token cost +- Recommend Markdown over HTML in page content descriptions (~3x fewer tokens) + +##### QA & Documentation +- Add `CLAUDE.md` project knowledge base for AI-assisted development +- Add `KNOWN_ISSUES.md` documenting 16 pre-existing issues found during QA audit +- Guard against whitespace-only markdown headings in fallback name generation +- Limit HTML text extraction to 2000-char prefix for performance - Update node-level description to communicate content hierarchy (Shelves > Books > Chapters > Pages) - Add CLAUDE.md project knowledge base for AI-assisted development diff --git a/CLAUDE.md b/CLAUDE.md index 1039aed..bad2a7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -305,6 +305,22 @@ The `dist/` directory is what n8n loads at runtime. --- +## Known Issues + +See `KNOWN_ISSUES.md` for a full list of 16 pre-existing issues found during QA audit. +The top 3 most impactful: + +1. **Tags don't support name:value pairs** - `"topic:networking"` becomes + `{name: "topic:networking"}` instead of `{name: "topic", value: "networking"}`. + The `{tag:name=value}` search syntax won't work with these tags. +2. **Audit-log sort baked into URL** - `sort=-created_at` is in the URL path string + instead of the query parameters object. Fragile but works. +3. **Attachment/Image limit ignores returnAll** - The `.map()` in these description + files overwrites `displayOptions` instead of merging, so Limit stays visible + when Return All is on. + +--- + ## Common Pitfalls 1. **`getNodeParameter` fallback**: Always pass `undefined` as third arg for optional fields, @@ -330,13 +346,32 @@ The `dist/` directory is what n8n loads at runtime. 7. **Search type filter**: The node appends `{type:X}` to the search query string. BookStack type values: `page`, `chapter`, `book`, `bookshelf` (note: "bookshelf", not "shelf"). +8. **`new Bookstack()` in execute()**: The `execute()` method has `this: IExecuteFunctions` + (n8n binds `this` to the execution context, not the class). All instance methods are + called via `const nodeInstance = new Bookstack()`. This works because all needed state + (`resourceEndpoints`, `resourceFields`) comes from class field declarations, not + constructor parameters. + +9. **Audit-log sort parameter**: The sort is baked into the URL string + (`'/audit-log?sort=-created_at'`). Don't add a separate `sort` to `qs` or it will + conflict. See KNOWN_ISSUES.md #2. + +10. **`decodeHtmlEntities` limitations**: Only used for fallback name generation. + Does not handle hex entities (except `'`), `'`, or case-insensitive + entity names (except ` `). Double-decodes `&lt;` to `<`. Acceptable + for its limited scope but don't reuse for general HTML processing. + +11. **NodeApiError detection**: Uses string comparison (`e?.constructor?.name === 'NodeApiError'`) + which could break under minification. If you need to modify error handling, consider + using `instanceof` with proper imports instead. + --- ## Version History | Version | Key Changes | |---------|-------------| -| 1.4.0 | Optional name on Create, auto-name generation, AI-optimized descriptions, CLAUDE.md | +| 1.4.0 | Optional name on Create, auto-name generation, AI-optimized descriptions, token-efficient navigation strategy, markdown-first, CLAUDE.md, KNOWN_ISSUES.md | | 1.3.0 | Attachment resource support with CRUD and multipart handling | | 1.2.0 | Image resource support with CRUD and multipart handling | | 1.1.0 | Switch to pnpm | diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md new file mode 100644 index 0000000..39d3ef7 --- /dev/null +++ b/KNOWN_ISSUES.md @@ -0,0 +1,220 @@ +# Known Issues + +Pre-existing issues found during QA audit (v1.4.0). None of these are regressions +from v1.4.0 changes - all exist in earlier versions. + +--- + +## MEDIUM Severity + +### 1. Tags do not support BookStack name:value pairs + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 99-101 + +**Description:** The description text in multiple files uses examples like +`"topic:networking, status:reviewed"`, implying name:value tag pairs. However, +`buildRequestBody` converts tags via: + +```typescript +body.tags.split(',').map((t: string) => ({ name: t.trim() })) +``` + +This means `"topic:networking"` becomes `{name: "topic:networking"}` instead of +`{name: "topic", value: "networking"}`. BookStack's API uses `name` and `value` +as separate properties for tag filtering (`{tag:topic=networking}`). + +**Impact:** Tags with colon-separated values are stored as literal tag names. The +`{tag:name=value}` search syntax won't work with these tags. + +**Suggested Fix:** Split each tag on the first `:` to produce `{name, value}` objects: + +```typescript +body.tags = body.tags.split(',').map((t: string) => { + const trimmed = t.trim(); + const colonIdx = trimmed.indexOf(':'); + if (colonIdx > 0) { + return { name: trimmed.slice(0, colonIdx), value: trimmed.slice(colonIdx + 1) }; + } + return { name: trimmed }; +}); +``` + +--- + +### 2. Audit-log endpoint has query parameter baked into the URL path + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 382 + +**Description:** The endpoint string is `'/audit-log?sort=-created_at'` with additional +`qs` parameters (`count`, `offset`) passed separately. The HTTP library constructs: +`baseUrl/audit-log?sort=-created_at&count=50&offset=0` + +While most HTTP libraries handle this correctly by appending with `&`, it is fragile +and unconventional. + +**Suggested Fix:** Move `sort` into the `qs` object: + +```typescript +const qs = { count, offset, sort: '-created_at' } as IDataObject; +const res = await bookstackApiRequest.call(context, 'GET', '/audit-log', {}, qs); +``` + +--- + +### 3. Attachment and Image limit field ignores returnAll condition + +**Files:** +- `nodes/Bookstack/descriptions/Attachment.description.ts`, lines 216-224 +- `nodes/Bookstack/descriptions/Image.description.ts`, lines 123-131 + +**Description:** Both files use `.map()` which completely **replaces** the original +`displayOptions` from `ListOperations.ts`. The `limit` field has +`displayOptions: { show: { returnAll: [false] } }` to hide when "Return All" is enabled. +For attachments and images, this condition is lost. + +Compare with Book/Page/Chapter/Shelf descriptions which correctly use: +```typescript +...(op.displayOptions?.show ?? {}) +``` + +**Impact:** The Limit field remains visible in the UI even when "Return All" is toggled on +for Attachment and Image resources. + +**Suggested Fix:** Use the same spread pattern as the other description files. + +--- + +### 4. handleAuditLogOperation lacks Array.isArray safety check + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 386 + +**Description:** The line casts directly without verifying the result is an array: + +```typescript +const data: JsonObject[] = (res?.data ?? res) as JsonObject[]; +``` + +Compare with `handleGetAllOperation` which safely checks: +```typescript +Array.isArray(data) ? (data as JsonObject[]) : [] +``` + +**Impact:** If the API response shape changes or returns an error object, +`aggregated.push(...data)` would throw a runtime error. + +--- + +## LOW Severity + +### 5. No filename sanitization in multipart upload + +**File:** `nodes/Bookstack/utils/BookstackApiHelpers.ts`, line 101 + +The `fileName` from `binaryData.fileName` is interpolated directly into the +`Content-Disposition` header. If the filename contains double quotes or CRLF +characters, it could break multipart parsing. Unlikely in practice. + +--- + +### 6. No trailing-slash normalization on baseUrl + +**File:** `nodes/Bookstack/utils/BookstackApiHelpers.ts`, line 46 + +If the user configures `baseUrl` as `https://example.com/api/` (with trailing slash), +the resulting URL will have a double slash: `https://example.com/api//pages`. +Most web servers handle this, but a `.replace(/\/+$/, '')` on `baseUrl` would be cleaner. + +--- + +### 7. HTML heading regex does not enforce matching tag levels + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 143 + +The regex `/]*>([\s\S]*?)<\/h[1-6]>/i` would match `

text

` +(mismatched levels). Extremely unlikely with BookStack-generated HTML. Only affects +fallback name generation. + +--- + +### 8. decodeHtmlEntities inconsistent case sensitivity + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 115-123 + +` ` is matched with `/gi` (case-insensitive) but other entities use `/g` +(case-sensitive). So `&NBSP;` decodes but `&` does not. In practice HTML +entities are almost always lowercase. + +--- + +### 9. decodeHtmlEntities does not handle hex numeric entities + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 123 + +Only decimal numeric entities (`A`) are handled. Hex entities like `A` +are not decoded (except the explicitly handled `'`). Acceptable since the +method is only used for fallback name generation. + +--- + +### 10. decodeHtmlEntities missing ' entity + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 114-124 + +Handles `'` and `'` (apostrophe) but not the named entity `'`. +Valid XML/HTML5 but less common in BookStack output. + +--- + +### 11. NodeApiError detection via string comparison + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 753 + +`e?.constructor?.name === 'NodeApiError'` relies on class name surviving minification. +Using `instanceof NodeApiError` (with import) would be safer. + +--- + +### 12. Null error in catch block could cause secondary TypeError + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 756 + +If `error` is `null` (extremely unlikely), `e.message` would throw TypeError. +Using `e?.message` would be more defensive. + +--- + +## INFO Severity + +### 13. tsconfig.json references non-existent .eslintrc.js + +**File:** `tsconfig.json`, line 28 + +The `include` array references `./.eslintrc.js` which does not exist. Silently +ignored by TypeScript but represents stale configuration. + +--- + +### 14. Type aliases add no type safety + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 26-27 + +`SearchItemMinimal` and `ContentResponseShape` are both aliases for `IDataObject`. +Consider using proper interfaces with defined properties. + +--- + +### 15. handleUpdateImageOperation can send empty multipart request + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 576-592 + +If `name` and `binaryPropertyName` are both empty, a multipart form with zero fields +is sent. BookStack API would return a validation error. + +--- + +### 16. Double-decoding potential in decodeHtmlEntities + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 114-124 + +Replacement order means `&lt;` becomes `<` then `<` (double-decoded). +Acceptable for fallback name generation but not faithful HTML entity decoding. From 5da1a97eab8bc1878394b9e47d9bc536835fd129 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 15:24:24 +0000 Subject: [PATCH 07/20] fix: clean up action texts - move AI guidance from titles to field descriptions Action texts are shown as dropdown labels in the n8n UI and should be short and clean. The long warnings and navigation hints were cluttering the operation picker. Moved all AI guidance to field-level description properties where they belong (visible to AI agents via MCP, but not cluttering the UI dropdown). Before: "List pages (WARNING: use Search instead to find pages...)" After: "Get many pages" AI guidance remains in: node description, field descriptions for Search Query, Return All, Limit, Deep Dive, and CLAUDE.md. https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- nodes/Bookstack/Bookstack.node.ts | 2 +- nodes/Bookstack/descriptions/Book.description.ts | 6 +++--- nodes/Bookstack/descriptions/Chapter.description.ts | 8 ++++---- nodes/Bookstack/descriptions/Global.description.ts | 2 +- nodes/Bookstack/descriptions/Page.description.ts | 8 ++++---- nodes/Bookstack/descriptions/Shelf.description.ts | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index aba977d..ad035e1 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -34,7 +34,7 @@ export class Bookstack implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Manage BookStack content (Shelves > Books > Chapters > Pages). IMPORTANT: To find content, always use Search first (returns only IDs and previews), then Get single items by ID. NEVER use Get Many with Return All on large instances - it loads everything and wastes massive tokens. Strategy: Search → Get by ID → Update/Create.', + description: 'Manage BookStack content (Shelves > Books > Chapters > Pages)', defaults: { name: 'Bookstack' }, inputs: ['main'], outputs: ['main'], diff --git a/nodes/Bookstack/descriptions/Book.description.ts b/nodes/Bookstack/descriptions/Book.description.ts index dccda0c..c8ea6ca 100644 --- a/nodes/Bookstack/descriptions/Book.description.ts +++ b/nodes/Bookstack/descriptions/Book.description.ts @@ -13,10 +13,10 @@ export const bookOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create a book (contains chapters and pages)' }, + { name: 'Create', value: 'create', action: 'Create a book' }, { name: 'Delete', value: 'delete', action: 'Delete a book' }, - { name: 'Get', value: 'get', action: 'Get a single book by ID with its table of contents (chapters and pages listed, not full content)' }, - { name: 'Get Many', value: 'getAll', action: 'List books (prefer Search to find books by keyword instead)' }, + { name: 'Get', value: 'get', action: 'Get a book' }, + { name: 'Get Many', value: 'getAll', action: 'Get many books' }, { name: 'Update', value: 'update', action: 'Update a book' }, ], default: 'getAll', diff --git a/nodes/Bookstack/descriptions/Chapter.description.ts b/nodes/Bookstack/descriptions/Chapter.description.ts index beaf157..cb6e68b 100644 --- a/nodes/Bookstack/descriptions/Chapter.description.ts +++ b/nodes/Bookstack/descriptions/Chapter.description.ts @@ -13,11 +13,11 @@ export const chapterOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create a chapter inside a book (groups related pages)' }, + { name: 'Create', value: 'create', action: 'Create a chapter' }, { name: 'Delete', value: 'delete', action: 'Delete a chapter' }, - { name: 'Get', value: 'get', action: 'Get a single chapter by ID with its list of pages (use after Search)' }, - { name: 'Get Many', value: 'getAll', action: 'List chapters (prefer Search to find chapters by keyword instead)' }, - { name: 'Update', value: 'update', action: 'Update a chapter or move it to a different book' }, + { name: 'Get', value: 'get', action: 'Get a chapter' }, + { name: 'Get Many', value: 'getAll', action: 'Get many chapters' }, + { name: 'Update', value: 'update', action: 'Update a chapter' }, ], default: 'getAll', }, diff --git a/nodes/Bookstack/descriptions/Global.description.ts b/nodes/Bookstack/descriptions/Global.description.ts index 3fc6d72..6e34556 100644 --- a/nodes/Bookstack/descriptions/Global.description.ts +++ b/nodes/Bookstack/descriptions/Global.description.ts @@ -12,7 +12,7 @@ export const globalOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Search', value: 'search', action: 'Search content by keywords - ALWAYS use this first instead of Get Many (returns only IDs, names, and previews - very token-efficient)' }, + { name: 'Search', value: 'search', action: 'Global search' }, { name: 'Audit Log', value: 'auditLogList', action: 'Audit log' }, ], default: 'search', diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index 42f7fae..a1e426c 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -13,11 +13,11 @@ export const pageOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create a page in a book or chapter (prefer markdown over html)' }, + { name: 'Create', value: 'create', action: 'Create a page' }, { name: 'Delete', value: 'delete', action: 'Delete a page' }, - { name: 'Get', value: 'get', action: 'Get a single page by ID with full content (use after Search to read specific pages)' }, - { name: 'Get Many', value: 'getAll', action: 'List pages (WARNING: use Search instead to find pages - Get Many on large instances wastes tokens)' }, - { name: 'Update', value: 'update', action: 'Update a page or move it to another book/chapter' }, + { name: 'Get', value: 'get', action: 'Get a page' }, + { name: 'Get Many', value: 'getAll', action: 'Get many pages' }, + { name: 'Update', value: 'update', action: 'Update a page' }, ], default: 'getAll', }, diff --git a/nodes/Bookstack/descriptions/Shelf.description.ts b/nodes/Bookstack/descriptions/Shelf.description.ts index cd35b39..ad5e041 100644 --- a/nodes/Bookstack/descriptions/Shelf.description.ts +++ b/nodes/Bookstack/descriptions/Shelf.description.ts @@ -13,10 +13,10 @@ export const shelfOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create a shelf (top-level container for books)' }, + { name: 'Create', value: 'create', action: 'Create a shelf' }, { name: 'Delete', value: 'delete', action: 'Delete a shelf' }, - { name: 'Get', value: 'get', action: 'Get a single shelf by ID with its list of books (use after Search)' }, - { name: 'Get Many', value: 'getAll', action: 'List shelves (prefer Search to find shelves by keyword instead)' }, + { name: 'Get', value: 'get', action: 'Get a shelf' }, + { name: 'Get Many', value: 'getAll', action: 'Get many shelves' }, { name: 'Update', value: 'update', action: 'Update a shelf' }, ], default: 'getAll', From 76b23ae9d7f3d31058cf49ce1f8fd599ee69e0fb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 17:08:24 +0000 Subject: [PATCH 08/20] fix: rewrite tool descriptions for 100% LLM correctness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each tool description must be self-contained so an LLM knows exactly how to use it without external context. Key improvements: - Node description: explains full hierarchy (Shelves > Books > Chapters > Pages) and the Search → Get → Update workflow strategy - Get operations: describe what the response CONTAINS (e.g. "returns book with contents array listing chapters and pages") - Page Create: clarifies EITHER book_id OR chapter_id (not both) - Page Create: instructs AI to generate descriptive titles - Search: documents exact response fields (id, name, type, url, preview_html, tags) and provides query example with filters - Move semantics: clarifies that moving removes from current location - Filter fields: lists available field names (name, created_at, etc.) https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- nodes/Bookstack/Bookstack.node.ts | 2 +- nodes/Bookstack/descriptions/Book.description.ts | 4 ++-- nodes/Bookstack/descriptions/Chapter.description.ts | 4 ++-- nodes/Bookstack/descriptions/Global.description.ts | 2 +- nodes/Bookstack/descriptions/ListOperations.ts | 2 +- nodes/Bookstack/descriptions/Page.description.ts | 8 ++++---- nodes/Bookstack/descriptions/Shelf.description.ts | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index ad035e1..6954e2a 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -34,7 +34,7 @@ export class Bookstack implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Manage BookStack content (Shelves > Books > Chapters > Pages)', + description: 'Manage BookStack content. Hierarchy: Shelves contain Books, Books contain Chapters and Pages, Chapters contain Pages. To find content, use Global Search first (returns IDs and previews). Then use Get by ID to read full content. Use Update to move content by changing parent IDs. Prefer markdown over HTML for page content.', defaults: { name: 'Bookstack' }, inputs: ['main'], outputs: ['main'], diff --git a/nodes/Bookstack/descriptions/Book.description.ts b/nodes/Bookstack/descriptions/Book.description.ts index c8ea6ca..fa07b38 100644 --- a/nodes/Bookstack/descriptions/Book.description.ts +++ b/nodes/Bookstack/descriptions/Book.description.ts @@ -36,7 +36,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Numeric ID of the book. Use Search or Get Many to find book IDs.', + description: 'Numeric ID of the book. Returns the book with its table of contents: a "contents" array listing all chapters and direct pages (names and IDs, not full page content).', placeholder: 'Enter book ID (e.g., 123)', }, { @@ -50,7 +50,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Book title (max 255 chars). If omitted, auto-generated from description or timestamp.', + description: 'Book title (max 255 chars). The AI should generate a concise, descriptive title. If omitted, auto-generated from description or timestamp.', }, { displayName: 'Name', diff --git a/nodes/Bookstack/descriptions/Chapter.description.ts b/nodes/Bookstack/descriptions/Chapter.description.ts index cb6e68b..7ff4097 100644 --- a/nodes/Bookstack/descriptions/Chapter.description.ts +++ b/nodes/Bookstack/descriptions/Chapter.description.ts @@ -36,7 +36,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Numeric ID of the chapter. Use Search or Get Many to find chapter IDs.', + description: 'Numeric ID of the chapter. Returns the chapter with a "pages" array listing all pages in it (names and IDs, not full page content).', placeholder: 'Enter chapter ID (e.g., 101)', }, { @@ -79,7 +79,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Chapter title (max 255 chars). If omitted, auto-generated from description or timestamp.', + description: 'Chapter title (max 255 chars). The AI should generate a concise, descriptive title. If omitted, auto-generated from description or timestamp.', placeholder: 'Enter chapter name', }, { diff --git a/nodes/Bookstack/descriptions/Global.description.ts b/nodes/Bookstack/descriptions/Global.description.ts index 6e34556..f755452 100644 --- a/nodes/Bookstack/descriptions/Global.description.ts +++ b/nodes/Bookstack/descriptions/Global.description.ts @@ -38,7 +38,7 @@ export const globalFields: INodeProperties[] = [ }, placeholder: 'Enter search terms', description: - 'Search terms to find content. Returns only IDs, names, and short previews (token-efficient). Supports filters: {type:page}, {in_name:text}, {in_body:text}, {tag:tagname}, {tag:name=value}, {created_by:id}, {updated_by:id}. Workflow: 1) Search here to find candidates, 2) Get individual items by ID to read full content, 3) Update or Create as needed. NEVER use Get Many to browse - always search first.', + 'Search terms to find content. Returns a list of matches with: id, name, type (page/book/chapter/bookshelf), url, preview_html, tags. Does NOT return full page content - use Get Page by ID for that. Supports inline filters in the query string: {type:page}, {type:book}, {type:chapter}, {in_name:text}, {in_body:text}, {tag:tagname}, {tag:name=value}, {created_by:id}, {updated_by:id}. Example: "networking {type:page} {tag:topic:networking}".', }, { displayName: 'Content Type Filter', diff --git a/nodes/Bookstack/descriptions/ListOperations.ts b/nodes/Bookstack/descriptions/ListOperations.ts index f6cfc77..4bdd4ea 100644 --- a/nodes/Bookstack/descriptions/ListOperations.ts +++ b/nodes/Bookstack/descriptions/ListOperations.ts @@ -44,7 +44,7 @@ export const listOperations: INodeProperties[] = [ name: 'field', type: 'string', default: '', - description: 'Field to filter on', + description: 'Field to filter on (e.g. name, created_at, updated_at, book_id, chapter_id)', }, { displayName: 'Operation', diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index a1e426c..80a1a01 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -36,7 +36,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Numeric ID of the page. Use Search or Get Many to find page IDs.', + description: 'Numeric ID of the page. Returns the full page including html, markdown, tags, book_id, chapter_id, and metadata.', placeholder: 'Enter page ID (e.g., 456)', }, { @@ -50,7 +50,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Page title (max 255 chars). If omitted, auto-generated from the first heading or first line of content.', + description: 'Page title (max 255 chars). The AI should generate a concise, descriptive title from the content. If omitted, auto-generated from the first heading or first line of content.', }, { displayName: 'Name', @@ -76,7 +76,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the parent book. Required on create if chapter_id is not set. On update, set this to MOVE the page to a different book.', + description: 'ID of the parent book. On create: set EITHER book_id OR chapter_id (not both). On update: set this to MOVE the page to a different book (removes it from its current chapter).', }, { displayName: 'Chapter ID', @@ -89,7 +89,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the parent chapter. Required on create if book_id is not set. On update, set this to MOVE the page into a different chapter.', + description: 'ID of the parent chapter. On create: set EITHER chapter_id OR book_id (not both). On update: set this to MOVE the page into a different chapter.', }, { displayName: 'HTML Content', diff --git a/nodes/Bookstack/descriptions/Shelf.description.ts b/nodes/Bookstack/descriptions/Shelf.description.ts index ad5e041..e3e5b27 100644 --- a/nodes/Bookstack/descriptions/Shelf.description.ts +++ b/nodes/Bookstack/descriptions/Shelf.description.ts @@ -36,7 +36,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Numeric ID of the shelf. Use Search or Get Many to find shelf IDs.', + description: 'Numeric ID of the shelf. Returns the shelf with a "books" array listing all books assigned to it (names and IDs).', placeholder: 'Enter shelf ID (e.g., 789)', }, { @@ -50,7 +50,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Shelf title (max 255 chars). If omitted, auto-generated from description or timestamp.', + description: 'Shelf title (max 255 chars). The AI should generate a concise, descriptive title. If omitted, auto-generated from description or timestamp.', placeholder: 'Enter shelf name', }, { From f297cef14a645070b78b2aa13e69e4b819a09a98 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 17:13:25 +0000 Subject: [PATCH 09/20] fix: close 8 LLM description gaps found in tool-use audit Simulated an LLM using the tools for 10 real tasks and found gaps where the LLM would have to guess. Fixes: 1. Tags: remove misleading name:value examples (code doesn't split on colon - known issue). Use simple tag names instead. 2. Tags: document that update REPLACES all existing tags. 3. Markdown/HTML: document that update REPLACES entire content. To append, first Get, merge, then Update. 4. Page move: clarify that setting chapter_id alone is sufficient (no need to clear book_id). 5. Delete: document cascading behavior in node description (deleting a book deletes all chapters and pages). 6. Search: clarify that Content Type Filter is auto-appended, do NOT also add {type:...} manually to avoid double filtering. 7. Search: remove confusing {tag:name=value} example (doesn't work with current tag implementation). 8. HTML field: document same replace-on-update semantics as markdown. https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- nodes/Bookstack/Bookstack.node.ts | 2 +- nodes/Bookstack/descriptions/Book.description.ts | 2 +- nodes/Bookstack/descriptions/Chapter.description.ts | 2 +- nodes/Bookstack/descriptions/Global.description.ts | 4 ++-- nodes/Bookstack/descriptions/Page.description.ts | 8 ++++---- nodes/Bookstack/descriptions/Shelf.description.ts | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index 6954e2a..7bd2b96 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -34,7 +34,7 @@ export class Bookstack implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Manage BookStack content. Hierarchy: Shelves contain Books, Books contain Chapters and Pages, Chapters contain Pages. To find content, use Global Search first (returns IDs and previews). Then use Get by ID to read full content. Use Update to move content by changing parent IDs. Prefer markdown over HTML for page content.', + description: 'Manage BookStack content. Hierarchy: Shelves contain Books, Books contain Chapters and Pages, Chapters contain Pages. To find content, use Global Search first (returns IDs and previews). Then Get by ID to read full content. Use Update to move content by changing parent IDs. Delete is permanent and cascading (deleting a book deletes all its chapters and pages). Prefer markdown over HTML for page content.', defaults: { name: 'Bookstack' }, inputs: ['main'], outputs: ['main'], diff --git a/nodes/Bookstack/descriptions/Book.description.ts b/nodes/Bookstack/descriptions/Book.description.ts index fa07b38..942c53a 100644 --- a/nodes/Bookstack/descriptions/Book.description.ts +++ b/nodes/Bookstack/descriptions/Book.description.ts @@ -102,7 +102,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags (e.g. "category:devops, status:active"). Tags enable search via {tag:name} syntax and are useful for categorization.', + description: 'Comma-separated tags (e.g. "devops, infrastructure, active"). On update, this REPLACES all existing tags. Tags are searchable via {tag:tagname} in Global Search.', }, ...listOperations.map((op) => ({ ...op, diff --git a/nodes/Bookstack/descriptions/Chapter.description.ts b/nodes/Bookstack/descriptions/Chapter.description.ts index 7ff4097..cd63612 100644 --- a/nodes/Bookstack/descriptions/Chapter.description.ts +++ b/nodes/Bookstack/descriptions/Chapter.description.ts @@ -121,7 +121,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags (e.g. "topic:networking, status:reviewed"). Tags enable search via {tag:name} syntax.', + description: 'Comma-separated tags (e.g. "networking, reviewed"). On update, this REPLACES all existing tags. Tags are searchable via {tag:tagname} in Global Search.', placeholder: 'tag1, tag2, tag3', }, ...listOperations.map((op) => ({ diff --git a/nodes/Bookstack/descriptions/Global.description.ts b/nodes/Bookstack/descriptions/Global.description.ts index f755452..21a5af9 100644 --- a/nodes/Bookstack/descriptions/Global.description.ts +++ b/nodes/Bookstack/descriptions/Global.description.ts @@ -38,7 +38,7 @@ export const globalFields: INodeProperties[] = [ }, placeholder: 'Enter search terms', description: - 'Search terms to find content. Returns a list of matches with: id, name, type (page/book/chapter/bookshelf), url, preview_html, tags. Does NOT return full page content - use Get Page by ID for that. Supports inline filters in the query string: {type:page}, {type:book}, {type:chapter}, {in_name:text}, {in_body:text}, {tag:tagname}, {tag:name=value}, {created_by:id}, {updated_by:id}. Example: "networking {type:page} {tag:topic:networking}".', + 'Search terms to find content. Returns a list of matches with: id, name, type (page/book/chapter/bookshelf), url, preview_html, tags. Does NOT return full page content - use Get by ID for that. Use the Content Type Filter below instead of adding {type:...} to the query (it is added automatically). Additional inline query filters: {in_name:text}, {in_body:text}, {tag:tagname}, {created_by:id}, {updated_by:id}. Example: "networking {in_name:setup}".', }, { displayName: 'Content Type Filter', @@ -59,7 +59,7 @@ export const globalFields: INodeProperties[] = [ ], default: 'all', description: - 'Filter search results by content type. This filter will be automatically added to your search query.', + 'Filter by content type. Automatically appended to the query - do NOT also add {type:...} manually. Use "Pages" to find pages, "Books" for books, etc.', }, { displayName: 'Return All', diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index 80a1a01..0e877ea 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -89,7 +89,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the parent chapter. On create: set EITHER chapter_id OR book_id (not both). On update: set this to MOVE the page into a different chapter.', + description: 'ID of the parent chapter. On create: set EITHER chapter_id OR book_id (not both). On update: setting chapter_id alone moves the page into that chapter (no need to clear book_id).', }, { displayName: 'HTML Content', @@ -102,7 +102,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'HTML body of the page. Only use this if markdown is not suitable. Prefer markdown over HTML - it uses fewer tokens and is more efficient for AI processing. Required on create if markdown is not set. Do NOT set both html and markdown.', + description: 'HTML body of the page. Only use if markdown is not suitable - markdown uses ~3x fewer tokens. Required on create if markdown is not set. On update, REPLACES the entire page content. Do NOT set both html and markdown.', }, { displayName: 'Markdown Content', @@ -115,7 +115,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Markdown body of the page. PREFERRED over HTML - uses ~3x fewer tokens and is more efficient for AI processing. Required on create if html is not set. Do NOT set both html and markdown.', + description: 'Markdown body of the page. PREFERRED over HTML - uses ~3x fewer tokens. Required on create if html is not set. On update, REPLACES the entire page content. To append, first Get the page, merge content, then Update. Do NOT set both html and markdown.', }, { displayName: 'Tags', @@ -128,7 +128,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags (e.g. "topic:networking, status:reviewed"). Tags enable search via {tag:name} syntax and are useful for AI categorization.', + description: 'Comma-separated tags (e.g. "networking, docker, reviewed"). On update, this REPLACES all existing tags. Tags are searchable via {tag:tagname} in Global Search.', }, ...listOperations.map((op) => ({ ...op, diff --git a/nodes/Bookstack/descriptions/Shelf.description.ts b/nodes/Bookstack/descriptions/Shelf.description.ts index e3e5b27..de99d42 100644 --- a/nodes/Bookstack/descriptions/Shelf.description.ts +++ b/nodes/Bookstack/descriptions/Shelf.description.ts @@ -106,7 +106,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags (e.g. "area:infrastructure, status:active"). Tags enable search via {tag:name} syntax.', + description: 'Comma-separated tags (e.g. "infrastructure, active"). On update, this REPLACES all existing tags. Tags are searchable via {tag:tagname} in Global Search.', placeholder: 'tag1, tag2, tag3', }, ...listOperations.map((op) => ({ From f39129e1de0a3f57f35d58354f98de5d92b85e5e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 17:19:07 +0000 Subject: [PATCH 10/20] docs: add archive-first best practice for AI agents Document recommended pattern where AI agents move unwanted content to an Archive shelf instead of deleting it permanently. A human can then review and delete manually from the archive. - CLAUDE.md: full archive workflow documentation with setup steps and tool usage for archiving pages, chapters, and books - Node description: brief "prefer moving to Archive shelf" hint - Covers shelf many-to-many book list management for archiving books Delete remains available as a tool - the recommendation is advisory, not enforced. Each user decides their own risk tolerance. https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- CLAUDE.md | 29 +++++++++++++++++++++++++++++ nodes/Bookstack/Bookstack.node.ts | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index bad2a7f..f9c9151 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -239,6 +239,35 @@ The primary AI use case is automated content organization: - Create new structure (Create Chapter/Book if needed) - Rename the page (Update with new `name`) - Tag for categorization (Update with `tags`) + - Archive instead of delete (see Best Practice below) + +### Archive-First Best Practice (Recommended for AI Agents) + +**Best practice: AI agents should move unwanted content to an "Archive" shelf +instead of deleting it.** A human can then review and permanently delete from +the archive manually. This prevents accidental data loss since delete is +permanent and cascading (deleting a book removes all its chapters and pages). + +Setup: +1. Create a shelf called "Archive" in BookStack (one-time, manual) +2. Optionally create an "Archive" book inside it for orphaned pages/chapters +3. In the n8n AI Agent, either omit the Delete tool entirely, or instruct the + agent via system prompt to prefer archiving over deletion + +How to archive with existing tools: +- **Archive a page**: Update Page with `chapter_id` or `book_id` pointing to + the Archive book/chapter +- **Archive a chapter**: Update Chapter with `book_id` pointing to the Archive book + (all pages inside move with it automatically) +- **Archive a book**: Update the source shelf's `books` list to remove the book, + then update the Archive shelf's `books` list to add it. Note: shelves use + many-to-many relationships, so you need to Get both shelves first to read + their current book lists, then Update each with the modified lists. + +If the Delete tool is provided to the AI agent, it can be used at the agent's +discretion - but be aware that deletes are permanent and cascading. There is no +undo via the API (BookStack has a recycle bin in the web UI, but it is not +accessible via API). ### Token-Efficient Navigation Strategy diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index 7bd2b96..62e2b6f 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -34,7 +34,7 @@ export class Bookstack implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Manage BookStack content. Hierarchy: Shelves contain Books, Books contain Chapters and Pages, Chapters contain Pages. To find content, use Global Search first (returns IDs and previews). Then Get by ID to read full content. Use Update to move content by changing parent IDs. Delete is permanent and cascading (deleting a book deletes all its chapters and pages). Prefer markdown over HTML for page content.', + description: 'Manage BookStack content. Hierarchy: Shelves contain Books, Books contain Chapters and Pages, Chapters contain Pages. To find content, use Global Search first (returns IDs and previews). Then Get by ID to read full content. Use Update to move content by changing parent IDs. Delete is permanent and cascading - prefer moving content to an Archive shelf instead. Prefer markdown over HTML for page content.', defaults: { name: 'Bookstack' }, inputs: ['main'], outputs: ['main'], From c06dcd7e416582bfb2d18b6d6b6cf798e5bf8a28 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 17:26:25 +0000 Subject: [PATCH 11/20] fix: final QA audit fixes - action texts, placeholder, known issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Findings from final QA audit with 3 parallel review agents: - Fix Attachment action texts: bare "Create" → "Create an attachment" (consistent with all other resources, important for AI/MCP) - Fix Image action texts: bare "Create" → "Create an image" - Fix Search limit placeholder: "100" → "20" (was contradicting the description guidance "keep low 5-20") - Document 2 new known issues: requiresDataPath on search query (#17), missing continueOnFail() support (#18) Total known issues: 18 (all pre-existing, none from v1.4.0) https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- KNOWN_ISSUES.md | 23 +++++++++++++++++++ .../descriptions/Attachment.description.ts | 10 ++++---- .../descriptions/Global.description.ts | 2 +- .../descriptions/Image.description.ts | 10 ++++---- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 39d3ef7..2818015 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -218,3 +218,26 @@ is sent. BookStack API would return a validation error. Replacement order means `&lt;` becomes `<` then `<` (double-decoded). Acceptable for fallback name generation but not faithful HTML entity decoding. + +--- + +### 17. `requiresDataPath: 'single'` on search query field + +**File:** `nodes/Bookstack/descriptions/Global.description.ts`, line 29 + +The `requiresDataPath: 'single'` property on the search query field is unusual for +a plain text search string. This property is typically used for expression-mode fields +that need data path resolution. On a plain search string field, this could cause +unexpected behavior in certain n8n execution contexts. May be intentional for AI +tool usage but warrants verification. + +--- + +### 18. No `continueOnFail()` support in execute() + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 747-758 + +The catch block always rethrows errors. It never checks `this.continueOnFail()`, +which is a common n8n pattern for allowing workflows to continue despite errors. +Items that fail will halt the entire node execution. This may be intentional for +data integrity but limits error resilience in batch workflows. diff --git a/nodes/Bookstack/descriptions/Attachment.description.ts b/nodes/Bookstack/descriptions/Attachment.description.ts index 440d09e..28faef3 100644 --- a/nodes/Bookstack/descriptions/Attachment.description.ts +++ b/nodes/Bookstack/descriptions/Attachment.description.ts @@ -13,11 +13,11 @@ export const attachmentOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create an attachment' }, + { name: 'Delete', value: 'delete', action: 'Delete an attachment' }, + { name: 'Get', value: 'get', action: 'Get an attachment' }, + { name: 'Get Many', value: 'getAll', action: 'Get many attachments' }, + { name: 'Update', value: 'update', action: 'Update an attachment' }, ], default: 'getAll', }, diff --git a/nodes/Bookstack/descriptions/Global.description.ts b/nodes/Bookstack/descriptions/Global.description.ts index 21a5af9..85fee45 100644 --- a/nodes/Bookstack/descriptions/Global.description.ts +++ b/nodes/Bookstack/descriptions/Global.description.ts @@ -91,7 +91,7 @@ export const globalFields: INodeProperties[] = [ maxValue: 100, }, description: 'Max results to return. Keep this low (5-20) when searching to save tokens. You can always search again with different terms if needed.', - placeholder: '100', + placeholder: '20', }, { displayName: 'Deep Dive', diff --git a/nodes/Bookstack/descriptions/Image.description.ts b/nodes/Bookstack/descriptions/Image.description.ts index 2089612..61b46ae 100644 --- a/nodes/Bookstack/descriptions/Image.description.ts +++ b/nodes/Bookstack/descriptions/Image.description.ts @@ -13,11 +13,11 @@ export const imageOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create an image' }, + { name: 'Delete', value: 'delete', action: 'Delete an image' }, + { name: 'Get', value: 'get', action: 'Get an image' }, + { name: 'Get Many', value: 'getAll', action: 'Get many images' }, + { name: 'Update', value: 'update', action: 'Update an image' }, ], default: 'getAll', }, From b5b6bdd8a42552d41f38d48d4eb96bd2e81f6acd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 17:27:46 +0000 Subject: [PATCH 12/20] fix: final audit cleanup - duplicate changelog, 2 new known issues - Remove duplicate "Add CLAUDE.md" line from CHANGELOG.md - Document known issue #19: multipart handler missing try/catch (attachment/image errors show raw unformatted messages) - Document known issue #20: redundant credential null checks - Update CLAUDE.md known issues count to 20 https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- CHANGELOG.md | 2 -- CLAUDE.md | 2 +- KNOWN_ISSUES.md | 30 ++++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef28608..b37a631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,8 +24,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Add `KNOWN_ISSUES.md` documenting 16 pre-existing issues found during QA audit - Guard against whitespace-only markdown headings in fallback name generation - Limit HTML text extraction to 2000-char prefix for performance -- Update node-level description to communicate content hierarchy (Shelves > Books > Chapters > Pages) -- Add CLAUDE.md project knowledge base for AI-assisted development #### [1.3.0](https://github.com/lucaguindani/n8n-nodes-bookstack/compare/1.2.0...1.3.0) diff --git a/CLAUDE.md b/CLAUDE.md index f9c9151..9fb6137 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -336,7 +336,7 @@ The `dist/` directory is what n8n loads at runtime. ## Known Issues -See `KNOWN_ISSUES.md` for a full list of 16 pre-existing issues found during QA audit. +See `KNOWN_ISSUES.md` for a full list of 20 pre-existing issues found during QA audit. The top 3 most impactful: 1. **Tags don't support name:value pairs** - `"topic:networking"` becomes diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 2818015..1404c93 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -241,3 +241,33 @@ The catch block always rethrows errors. It never checks `this.continueOnFail()`, which is a common n8n pattern for allowing workflows to continue despite errors. Items that fail will halt the entire node execution. This may be intentional for data integrity but limits error resilience in batch workflows. + +--- + +### 19. Multipart request handler has no try/catch error wrapping + +**File:** `nodes/Bookstack/utils/BookstackApiHelpers.ts`, line 122 + +The `bookstackApiRequest()` function wraps HTTP errors in `NodeApiError` for clean +error messages. The `bookstackApiRequestMultipart()` function does NOT - errors from +attachment/image uploads propagate as raw, unformatted exceptions. Users see +unfriendly error messages on upload failures. + +**Suggested Fix:** Add the same try/catch pattern: +```typescript +try { + return await this.helpers.httpRequestWithAuthentication.call(this, 'bookstackApi', options); +} catch (error) { + throw new NodeApiError(this.getNode(), error); +} +``` + +--- + +### 20. Redundant credential null checks + +**File:** `nodes/Bookstack/utils/BookstackApiHelpers.ts`, lines 32-35 and 67-70 + +`if (!credentials)` check after `await this.getCredentials('bookstackApi')`. The +`getCredentials()` method already throws if credentials are not found, so these +checks are dead code. Not harmful, just redundant. From d1632747b4c299b9c667f893801b394ab8ab8fc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 19:01:59 +0000 Subject: [PATCH 13/20] fix: provide complete list of filterable fields per resource The filter field description only listed 5 example fields. LLMs need the full list to know their options. Now documents all available fields grouped by resource: common fields (id, name, slug, created_at, etc.) and resource-specific fields (book_id, chapter_id, draft, template, priority, uploaded_to). https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- nodes/Bookstack/descriptions/ListOperations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodes/Bookstack/descriptions/ListOperations.ts b/nodes/Bookstack/descriptions/ListOperations.ts index 4bdd4ea..a81b9f5 100644 --- a/nodes/Bookstack/descriptions/ListOperations.ts +++ b/nodes/Bookstack/descriptions/ListOperations.ts @@ -7,7 +7,7 @@ export const listOperations: INodeProperties[] = [ type: 'string', default: '', placeholder: 'name, created_at, updated_at, ...', - description: 'Field to sort by', + description: 'Field name to sort by. Same fields available as for filtering (e.g. id, name, slug, created_at, updated_at, created_by).', }, { displayName: 'Sort Direction', @@ -44,7 +44,7 @@ export const listOperations: INodeProperties[] = [ name: 'field', type: 'string', default: '', - description: 'Field to filter on (e.g. name, created_at, updated_at, book_id, chapter_id)', + description: 'Field name to filter on. Available fields depend on the resource. Common: id, name, slug, created_at, updated_at, created_by, updated_by, owned_by. Pages also support: book_id, chapter_id, draft, template, priority. Chapters also support: book_id, priority. Attachments/Images also support: uploaded_to.', }, { displayName: 'Operation', From 7bc662e7bdfb3147a00d10c49553ec2d8c2863fc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 19:11:38 +0000 Subject: [PATCH 14/20] docs: add 4 missing findings to KNOWN_ISSUES.md (total: 24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add findings from final regression test that were not yet documented: - #21: !body.name falsy check too broad (v1.4.0, MEDIUM) - #22: required:true removal = silent accept for manual users (v1.4.0, MEDIUM) - #23: Page ID description misleading for delete (v1.4.0, LOW) - #24: README typo "lunch" → "launch" (pre-existing, LOW) Update header to distinguish v1.4.0 introductions from pre-existing issues. https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- KNOWN_ISSUES.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 1404c93..ac092f6 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -1,7 +1,7 @@ # Known Issues -Pre-existing issues found during QA audit (v1.4.0). None of these are regressions -from v1.4.0 changes - all exist in earlier versions. +Issues found during QA audit (v1.4.0). Items marked **(v1.4.0)** were introduced +by the v1.4.0 changes. All others are pre-existing. --- @@ -271,3 +271,50 @@ try { `if (!credentials)` check after `await this.getCredentials('bookstackApi')`. The `getCredentials()` method already throws if credentials are not found, so these checks are dead code. Not harmful, just redundant. + +--- + +### 21. `!body.name` falsy check is too broad **(v1.4.0)** + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 452 + +The check `if (!body.name && ...)` triggers for `undefined`, `null`, `''`, `0`, +`false`, and `NaN`. If an n8n expression somehow produces a numeric `0` or boolean +`false` as the name value, it would be overwritten by the fallback generator. + +A more precise check would be `body.name === undefined || body.name === ''`. +In practice this is extremely unlikely since `name` is a string field. + +--- + +### 22. Removing `required: true` from name silently accepts empty names **(v1.4.0)** + +**Files:** Page/Book/Chapter/Shelf description files + +Previously, the n8n UI blocked submission when the name field was empty (enforced +by `required: true`). Now the UI allows it and the node auto-generates a fallback +name (e.g. `page-2026-04-08T14-30-00`). Manual users who accidentally leave the +name empty will not get a validation error - they will get an auto-generated name +they may not notice until later. + +This is an intentional design decision for AI agent usage but changes behavior +for manual users. + +--- + +### 23. Page ID description is misleading for Delete operation **(v1.4.0)** + +**File:** `nodes/Bookstack/descriptions/Page.description.ts`, line 39 + +The ID field description says "Returns the full page including html, markdown, +tags..." - this is accurate for Get but misleading for Delete (which returns +a minimal or empty response). The ID field is shared across get, update, and +delete operations, so the description applies to all three. + +--- + +### 24. README typo: "lunch" instead of "launch" + +**File:** `README.md`, line 122 + +"To lunch a local instance" should be "To launch a local instance". From 14e1a5981a88a240320ce71d38ee06fb779a250f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 19:16:48 +0000 Subject: [PATCH 15/20] fix: document response fields for every tool so LLMs know what comes back Every tool description now documents: - Get: exact response fields (id, name, slug, created_at, tags[], etc.) - Get: child arrays with their shapes (contents[], pages[], books[]) - Create: confirms response contains the new id - Update: confirms response contains the updated object - Delete: confirms empty response on success - Search: fix field name (preview not preview_html), add empty=no matches Before: LLMs had to guess what the API returns. After: LLMs know exact field names for every operation. https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- nodes/Bookstack/descriptions/Book.description.ts | 2 +- nodes/Bookstack/descriptions/Chapter.description.ts | 2 +- nodes/Bookstack/descriptions/Global.description.ts | 2 +- nodes/Bookstack/descriptions/Page.description.ts | 4 ++-- nodes/Bookstack/descriptions/Shelf.description.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nodes/Bookstack/descriptions/Book.description.ts b/nodes/Bookstack/descriptions/Book.description.ts index 942c53a..fb6283e 100644 --- a/nodes/Bookstack/descriptions/Book.description.ts +++ b/nodes/Bookstack/descriptions/Book.description.ts @@ -36,7 +36,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Numeric ID of the book. Returns the book with its table of contents: a "contents" array listing all chapters and direct pages (names and IDs, not full page content).', + description: 'Numeric ID of the book. Get returns: id, name, slug, description, created_at, updated_at, created_by, updated_by, owned_by, tags[], and a "contents" array of {id, name, type, pages[]} for chapters and direct pages. Update returns the updated book. Delete returns empty on success.', placeholder: 'Enter book ID (e.g., 123)', }, { diff --git a/nodes/Bookstack/descriptions/Chapter.description.ts b/nodes/Bookstack/descriptions/Chapter.description.ts index cd63612..c918369 100644 --- a/nodes/Bookstack/descriptions/Chapter.description.ts +++ b/nodes/Bookstack/descriptions/Chapter.description.ts @@ -36,7 +36,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Numeric ID of the chapter. Returns the chapter with a "pages" array listing all pages in it (names and IDs, not full page content).', + description: 'Numeric ID of the chapter. Get returns: id, name, slug, book_id, description, priority, created_at, updated_at, created_by, updated_by, tags[], and a "pages" array of {id, name, slug, book_id, chapter_id, priority}. Update returns the updated chapter. Delete returns empty on success.', placeholder: 'Enter chapter ID (e.g., 101)', }, { diff --git a/nodes/Bookstack/descriptions/Global.description.ts b/nodes/Bookstack/descriptions/Global.description.ts index 85fee45..5a74b1f 100644 --- a/nodes/Bookstack/descriptions/Global.description.ts +++ b/nodes/Bookstack/descriptions/Global.description.ts @@ -38,7 +38,7 @@ export const globalFields: INodeProperties[] = [ }, placeholder: 'Enter search terms', description: - 'Search terms to find content. Returns a list of matches with: id, name, type (page/book/chapter/bookshelf), url, preview_html, tags. Does NOT return full page content - use Get by ID for that. Use the Content Type Filter below instead of adding {type:...} to the query (it is added automatically). Additional inline query filters: {in_name:text}, {in_body:text}, {tag:tagname}, {created_by:id}, {updated_by:id}. Example: "networking {in_name:setup}".', + 'Search terms to find content. Each result contains: id, name, type (page/book/chapter/bookshelf), url, preview (short text snippet), tags[], book, chapter. Does NOT return full page content - use Get by ID for that. Use the Content Type Filter below instead of adding {type:...} to the query. Additional inline filters: {in_name:text}, {in_body:text}, {tag:tagname}, {created_by:id}, {updated_by:id}. Example: "networking {in_name:setup}". An empty result list means no matches were found.', }, { displayName: 'Content Type Filter', diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index 0e877ea..c04ddc5 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -36,7 +36,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Numeric ID of the page. Returns the full page including html, markdown, tags, book_id, chapter_id, and metadata.', + description: 'Numeric ID of the page. Get returns: id, name, slug, html, markdown, book_id, chapter_id, priority, created_at, updated_at, created_by, updated_by, tags[]. Update returns the updated page. Delete returns an empty response on success.', placeholder: 'Enter page ID (e.g., 456)', }, { @@ -50,7 +50,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Page title (max 255 chars). The AI should generate a concise, descriptive title from the content. If omitted, auto-generated from the first heading or first line of content.', + description: 'Page title (max 255 chars). The AI should generate a concise, descriptive title from the content. If omitted, auto-generated from content. On success, returns the created page with its new id, name, slug, book_id, chapter_id.', }, { displayName: 'Name', diff --git a/nodes/Bookstack/descriptions/Shelf.description.ts b/nodes/Bookstack/descriptions/Shelf.description.ts index de99d42..b70a556 100644 --- a/nodes/Bookstack/descriptions/Shelf.description.ts +++ b/nodes/Bookstack/descriptions/Shelf.description.ts @@ -36,7 +36,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Numeric ID of the shelf. Returns the shelf with a "books" array listing all books assigned to it (names and IDs).', + description: 'Numeric ID of the shelf. Get returns: id, name, slug, description, created_at, updated_at, created_by, updated_by, tags[], and a "books" array of {id, name, slug}. Update returns the updated shelf. Delete returns empty on success.', placeholder: 'Enter shelf ID (e.g., 789)', }, { From 732d295bbc2d449481783c5028a69918bbca0e9e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 19:22:21 +0000 Subject: [PATCH 16/20] fix: resolve 3 remaining dissatisfaction items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Fix !body.name → (body.name === undefined || body.name === '') Prevents edge case where falsy values like 0 would trigger fallback. 2. Fix tags to support name:value pairs - "topic:networking" now produces {name: "topic", value: "networking"} instead of {name: "topic:networking"}. BookStack {tag:name=value} search now works correctly. This was Known Issue #1 - now fixed. 3. Shorten node description from 405 → 270 chars to prevent potential UI truncation while keeping all key information. Known Issues #1, #21, #23 marked as FIXED in v1.4.0. Tag descriptions updated to show name:value examples again. https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- CLAUDE.md | 9 ++-- KNOWN_ISSUES.md | 54 +++---------------- nodes/Bookstack/Bookstack.node.ts | 15 ++++-- .../descriptions/Book.description.ts | 2 +- .../descriptions/Chapter.description.ts | 2 +- .../descriptions/Page.description.ts | 2 +- .../descriptions/Shelf.description.ts | 2 +- 7 files changed, 27 insertions(+), 59 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9fb6137..d48f2cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -336,12 +336,11 @@ The `dist/` directory is what n8n loads at runtime. ## Known Issues -See `KNOWN_ISSUES.md` for a full list of 20 pre-existing issues found during QA audit. -The top 3 most impactful: +See `KNOWN_ISSUES.md` for a full list of issues found during QA audit (24 total, +3 fixed in v1.4.0, 21 remaining). The top 3 most impactful remaining issues: -1. **Tags don't support name:value pairs** - `"topic:networking"` becomes - `{name: "topic:networking"}` instead of `{name: "topic", value: "networking"}`. - The `{tag:name=value}` search syntax won't work with these tags. +1. ~~**Tags don't support name:value pairs**~~ **FIXED in v1.4.0** - Tags now split + on `:` to produce `{name: "topic", value: "networking"}`. 2. **Audit-log sort baked into URL** - `sort=-created_at` is in the URL path string instead of the query parameters object. Fragile but works. 3. **Attachment/Image limit ignores returnAll** - The `.map()` in these description diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index ac092f6..95d010d 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -7,37 +7,10 @@ by the v1.4.0 changes. All others are pre-existing. ## MEDIUM Severity -### 1. Tags do not support BookStack name:value pairs +### ~~1. Tags do not support BookStack name:value pairs~~ **FIXED in v1.4.0** -**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 99-101 - -**Description:** The description text in multiple files uses examples like -`"topic:networking, status:reviewed"`, implying name:value tag pairs. However, -`buildRequestBody` converts tags via: - -```typescript -body.tags.split(',').map((t: string) => ({ name: t.trim() })) -``` - -This means `"topic:networking"` becomes `{name: "topic:networking"}` instead of -`{name: "topic", value: "networking"}`. BookStack's API uses `name` and `value` -as separate properties for tag filtering (`{tag:topic=networking}`). - -**Impact:** Tags with colon-separated values are stored as literal tag names. The -`{tag:name=value}` search syntax won't work with these tags. - -**Suggested Fix:** Split each tag on the first `:` to produce `{name, value}` objects: - -```typescript -body.tags = body.tags.split(',').map((t: string) => { - const trimmed = t.trim(); - const colonIdx = trimmed.indexOf(':'); - if (colonIdx > 0) { - return { name: trimmed.slice(0, colonIdx), value: trimmed.slice(colonIdx + 1) }; - } - return { name: trimmed }; -}); -``` +Tags now support `name:value` pairs. Input `"topic:networking"` produces +`{name: "topic", value: "networking"}`. The `{tag:name=value}` search syntax works. --- @@ -274,16 +247,9 @@ checks are dead code. Not harmful, just redundant. --- -### 21. `!body.name` falsy check is too broad **(v1.4.0)** +### ~~21. `!body.name` falsy check is too broad~~ **FIXED in v1.4.0** -**File:** `nodes/Bookstack/Bookstack.node.ts`, line 452 - -The check `if (!body.name && ...)` triggers for `undefined`, `null`, `''`, `0`, -`false`, and `NaN`. If an n8n expression somehow produces a numeric `0` or boolean -`false` as the name value, it would be overwritten by the fallback generator. - -A more precise check would be `body.name === undefined || body.name === ''`. -In practice this is extremely unlikely since `name` is a string field. +The check now uses `body.name === undefined || body.name === ''` instead of `!body.name`. --- @@ -302,14 +268,10 @@ for manual users. --- -### 23. Page ID description is misleading for Delete operation **(v1.4.0)** - -**File:** `nodes/Bookstack/descriptions/Page.description.ts`, line 39 +### ~~23. Page ID description is misleading for Delete operation~~ **FIXED in v1.4.0** -The ID field description says "Returns the full page including html, markdown, -tags..." - this is accurate for Get but misleading for Delete (which returns -a minimal or empty response). The ID field is shared across get, update, and -delete operations, so the description applies to all three. +The ID field description now documents all three operations separately: +Get returns fields, Update returns updated object, Delete returns empty on success. --- diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index 62e2b6f..ae38727 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -34,7 +34,7 @@ export class Bookstack implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Manage BookStack content. Hierarchy: Shelves contain Books, Books contain Chapters and Pages, Chapters contain Pages. To find content, use Global Search first (returns IDs and previews). Then Get by ID to read full content. Use Update to move content by changing parent IDs. Delete is permanent and cascading - prefer moving content to an Archive shelf instead. Prefer markdown over HTML for page content.', + description: 'Manage BookStack content. Hierarchy: Shelves → Books → Chapters → Pages. Use Search to find content (returns IDs and previews), then Get by ID for full details. Update can move content by changing parent IDs. Delete is permanent and cascading. Prefer markdown over HTML.', defaults: { name: 'Bookstack' }, inputs: ['main'], outputs: ['main'], @@ -95,9 +95,16 @@ export class Bookstack implements INodeType { } } - // Convert tags to array format + // Convert tags to array format (supports "name" and "name:value" pairs) if (body.tags && typeof body.tags === 'string') { - body.tags = body.tags.split(',').map((t: string) => ({ name: t.trim() })); + body.tags = body.tags.split(',').map((t: string) => { + const trimmed = t.trim(); + const colonIdx = trimmed.indexOf(':'); + if (colonIdx > 0) { + return { name: trimmed.slice(0, colonIdx), value: trimmed.slice(colonIdx + 1) }; + } + return { name: trimmed }; + }); } // Convert books to array of integers @@ -449,7 +456,7 @@ export class Bookstack implements INodeType { } // Auto-generate name if not provided (BookStack API requires it) - if (!body.name && this.resourceFields[resource]?.includes('name')) { + if ((body.name === undefined || body.name === '') && this.resourceFields[resource]?.includes('name')) { body.name = this.generateFallbackName(resource, body); } diff --git a/nodes/Bookstack/descriptions/Book.description.ts b/nodes/Bookstack/descriptions/Book.description.ts index fb6283e..c4ddbdf 100644 --- a/nodes/Bookstack/descriptions/Book.description.ts +++ b/nodes/Bookstack/descriptions/Book.description.ts @@ -102,7 +102,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags (e.g. "devops, infrastructure, active"). On update, this REPLACES all existing tags. Tags are searchable via {tag:tagname} in Global Search.', + description: 'Comma-separated tags. Supports name:value pairs (e.g. "category:devops, status:active"). On update, this REPLACES all existing tags. Searchable via {tag:name} or {tag:name=value} in Global Search.', }, ...listOperations.map((op) => ({ ...op, diff --git a/nodes/Bookstack/descriptions/Chapter.description.ts b/nodes/Bookstack/descriptions/Chapter.description.ts index c918369..6a0152e 100644 --- a/nodes/Bookstack/descriptions/Chapter.description.ts +++ b/nodes/Bookstack/descriptions/Chapter.description.ts @@ -121,7 +121,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags (e.g. "networking, reviewed"). On update, this REPLACES all existing tags. Tags are searchable via {tag:tagname} in Global Search.', + description: 'Comma-separated tags. Supports name:value pairs (e.g. "topic:networking, status:reviewed"). On update, this REPLACES all existing tags. Searchable via {tag:name} or {tag:name=value} in Global Search.', placeholder: 'tag1, tag2, tag3', }, ...listOperations.map((op) => ({ diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index c04ddc5..6c7750d 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -128,7 +128,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags (e.g. "networking, docker, reviewed"). On update, this REPLACES all existing tags. Tags are searchable via {tag:tagname} in Global Search.', + description: 'Comma-separated tags. Supports name:value pairs (e.g. "topic:networking, status:reviewed"). On update, this REPLACES all existing tags. Searchable via {tag:name} or {tag:name=value} in Global Search.', }, ...listOperations.map((op) => ({ ...op, diff --git a/nodes/Bookstack/descriptions/Shelf.description.ts b/nodes/Bookstack/descriptions/Shelf.description.ts index b70a556..0e9cc20 100644 --- a/nodes/Bookstack/descriptions/Shelf.description.ts +++ b/nodes/Bookstack/descriptions/Shelf.description.ts @@ -106,7 +106,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags (e.g. "infrastructure, active"). On update, this REPLACES all existing tags. Tags are searchable via {tag:tagname} in Global Search.', + description: 'Comma-separated tags. Supports name:value pairs (e.g. "area:infrastructure, status:active"). On update, this REPLACES all existing tags. Searchable via {tag:name} or {tag:name=value} in Global Search.', placeholder: 'tag1, tag2, tag3', }, ...listOperations.map((op) => ({ From 509c17a8651fa36b9c3b05120dea7ddf3d23c9f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 19:27:18 +0000 Subject: [PATCH 17/20] fix: harden tag name:value parsing against edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter out empty tags from whitespace-only input, double commas, and trailing whitespace. Drop empty values (e.g. "name:" becomes {name: "name"} without an empty value field). Edge cases now handled: - " " (whitespace) → [] (empty array, no crash) - "tag1,,tag2" (double comma) → [{name:"tag1"},{name:"tag2"}] - "name:" (empty value) → {name:"name"} (no empty value sent) - "a:b:c" (multiple colons) → {name:"a", value:"b:c"} https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- nodes/Bookstack/Bookstack.node.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index ae38727..bd5585b 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -97,14 +97,19 @@ export class Bookstack implements INodeType { // Convert tags to array format (supports "name" and "name:value" pairs) if (body.tags && typeof body.tags === 'string') { - body.tags = body.tags.split(',').map((t: string) => { - const trimmed = t.trim(); - const colonIdx = trimmed.indexOf(':'); - if (colonIdx > 0) { - return { name: trimmed.slice(0, colonIdx), value: trimmed.slice(colonIdx + 1) }; - } - return { name: trimmed }; - }); + body.tags = body.tags + .split(',') + .map((t: string) => { + const trimmed = t.trim(); + if (!trimmed) return null; + const colonIdx = trimmed.indexOf(':'); + if (colonIdx > 0) { + const value = trimmed.slice(colonIdx + 1); + return value ? { name: trimmed.slice(0, colonIdx), value } : { name: trimmed.slice(0, colonIdx) }; + } + return { name: trimmed }; + }) + .filter((tag) => tag !== null); } // Convert books to array of integers From d64798acfc7fe65d8d6c40564463e04695759466 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 19:52:45 +0000 Subject: [PATCH 18/20] docs: final learnings from API consistency check Update CLAUDE.md with learnings from cross-referencing BookStack API docs, n8n tool system, and our implementation: - Tags pitfall updated: now documents name:value split and edge cases - New pitfall #12: IDs sent as strings (PHP coerces, but fragile) - New pitfall #13: search preview_html is object, not string - New pitfall #14: description_html exists but node only uses plaintext - New pitfall #15: API max pagination count is configurable per instance KNOWN_ISSUES.md updated with 3 new findings (#25-#27): - ID fields as strings instead of integers - Search preview_html shape mismatch - Missing API fields (description_html, priority, cover, chapter templates) Total: 27 known issues (3 fixed in v1.4.0, 24 remaining). https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- CLAUDE.md | 28 +++++++++++++++++++++++++--- KNOWN_ISSUES.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d48f2cf..45cf52c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -354,8 +354,11 @@ See `KNOWN_ISSUES.md` for a full list of issues found during QA audit (24 total, 1. **`getNodeParameter` fallback**: Always pass `undefined` as third arg for optional fields, not `''`. Empty string would be treated as a provided value. -2. **Tags format**: UI accepts comma-separated strings, but BookStack API expects - `[{name: "tag"}]`. The `buildRequestBody()` method handles conversion automatically. +2. **Tags format**: UI accepts comma-separated strings with optional `name:value` pairs + (e.g., `"topic:networking, status:active"`). The `buildRequestBody()` method splits + on the first `:` to produce `{name: "topic", value: "networking"}`. Tags without + a colon become `{name: "tagname"}`. Empty entries from double-commas or whitespace + are filtered out. 3. **Shelf books format**: UI accepts comma-separated IDs (e.g., "1,5,12"), converted to `[1, 5, 12]` integer array by `buildRequestBody()`. @@ -393,13 +396,32 @@ See `KNOWN_ISSUES.md` for a full list of issues found during QA audit (24 total, which could break under minification. If you need to modify error handling, consider using `instanceof` with proper imports instead. +12. **`book_id`/`chapter_id` sent as strings**: These ID fields are `type: 'string'` + in the UI but BookStack API expects integers. PHP/Laravel auto-coerces `"123"` to + `123`, so this works. If BookStack ever adds strict type checking, these would need + conversion to integers in `buildRequestBody()`. + +13. **Search `preview_html` is an object, not a string**: The BookStack search API + returns `preview_html` as `{name: string, content: string}` with HTML-highlighted + matches. The node maps it to `preview` in the output. It works but the shape is + different from what the description implies. + +14. **BookStack also has `description_html` (max 2000 chars)**: Books, chapters, and + shelves accept both `description` (plaintext, max 1900) and `description_html` + (HTML, max 2000). The node only exposes plaintext `description`. This is a feature + gap, not a bug. + +15. **API max pagination `count` is configurable**: CLAUDE.md and the code assume max + 500 per request (`API_MAX_ITEM_COUNT`). This is BookStack's default but instances + can configure it lower. The node paginates correctly regardless. + --- ## Version History | Version | Key Changes | |---------|-------------| -| 1.4.0 | Optional name on Create, auto-name generation, AI-optimized descriptions, token-efficient navigation strategy, markdown-first, CLAUDE.md, KNOWN_ISSUES.md | +| 1.4.0 | Optional name on Create, auto-name generation, AI-optimized descriptions, token-efficient navigation strategy, markdown-first, tag name:value support, CLAUDE.md, KNOWN_ISSUES.md | | 1.3.0 | Attachment resource support with CRUD and multipart handling | | 1.2.0 | Image resource support with CRUD and multipart handling | | 1.1.0 | Switch to pnpm | diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 95d010d..d725390 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -280,3 +280,35 @@ Get returns fields, Update returns updated object, Delete returns empty on succe **File:** `README.md`, line 122 "To lunch a local instance" should be "To launch a local instance". + +--- + +### 25. `book_id`/`chapter_id`/`default_template_id` sent as strings instead of integers + +**Files:** Page.description.ts, Chapter.description.ts, Book.description.ts + +These ID fields are `type: 'string'` in the n8n field definitions but the BookStack +API expects integers. PHP/Laravel auto-coerces `"123"` to `123` so this works in +practice, but it is technically sending the wrong type. + +--- + +### 26. Search result `preview_html` is an object, not a string + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 258 + +BookStack returns `preview_html` as `{name: string, content: string}` with +HTML-highlighted matches. The node maps it to `preview` in the output. The +description says "preview (short text snippet)" which is slightly inaccurate. + +--- + +### 27. Missing API fields not exposed by the node + +The following BookStack API fields exist but are not in the node: +- `description_html` (books, chapters, shelves - HTML variant, max 2000 chars) +- `priority` (pages, chapters - ordering within parent) +- `image` / `cover` (books, shelves - cover image) +- `default_template_id` for chapters (only exposed for books currently) + +These are feature gaps for potential future versions. From 125392275455763187e7b29b107f2afdfbcdbe90 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 14:00:14 +0000 Subject: [PATCH 19/20] feat: add Page Title (AI) field that supports $fromAI for AI agents The n8n framework does not show the "Defined automatically by the model" button on fields named "name" (likely an internal n8n restriction). This means AI agents cannot dynamically generate page titles via $fromAI. Workaround: Add a new field "Page Title (AI)" (parameter name: page_title) that DOES get the $fromAI button. When set, it overrides the Name field and is mapped to the API's "name" parameter. The page_title field is removed from the request body before sending (BookStack API only knows "name"). Priority: page_title > name > generateFallbackName > timestamp https://claude.ai/code/session_019hMoW1VKjD6QL4q7CcErjQ --- nodes/Bookstack/Bookstack.node.ts | 10 +++++++++- nodes/Bookstack/descriptions/Page.description.ts | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index bd5585b..af736fd 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -75,7 +75,7 @@ export class Bookstack implements INodeType { private readonly resourceFields: Record = { book: ['name', 'description', 'tags', 'default_template_id'], - page: ['name', 'html', 'markdown', 'book_id', 'chapter_id', 'tags'], + page: ['name', 'page_title', 'html', 'markdown', 'book_id', 'chapter_id', 'tags'], chapter: ['name', 'description', 'book_id', 'tags'], shelf: ['name', 'description', 'books', 'tags'], }; @@ -95,6 +95,14 @@ export class Bookstack implements INodeType { } } + // If page_title is set (AI-generated), use it as the name and remove the extra field + if (body.page_title && typeof body.page_title === 'string') { + body.name = body.page_title; + delete body.page_title; + } else { + delete body.page_title; + } + // Convert tags to array format (supports "name" and "name:value" pairs) if (body.tags && typeof body.tags === 'string') { body.tags = body.tags diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index 6c7750d..112f815 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -39,6 +39,19 @@ export const pageFields: INodeProperties[] = [ description: 'Numeric ID of the page. Get returns: id, name, slug, html, markdown, book_id, chapter_id, priority, created_at, updated_at, created_by, updated_by, tags[]. Update returns the updated page. Delete returns an empty response on success.', placeholder: 'Enter page ID (e.g., 456)', }, + { + displayName: 'Page Title (AI)', + name: 'page_title', + type: 'string', + displayOptions: { + show: { + resource: ['page'], + operation: ['create'], + }, + }, + default: '', + description: 'Page title generated by the AI model (max 255 chars). A concise, descriptive title that summarizes the page content. If set, this overrides the Name field below. If both are empty, a title is auto-generated from the content.', + }, { displayName: 'Name', name: 'name', @@ -50,7 +63,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Page title (max 255 chars). The AI should generate a concise, descriptive title from the content. If omitted, auto-generated from content. On success, returns the created page with its new id, name, slug, book_id, chapter_id.', + description: 'Page title for manual entry (max 255 chars). Only used if Page Title (AI) above is empty.', }, { displayName: 'Name', From a1a4957c1c410df08dd27b122554d749db56f9aa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 14:14:48 +0000 Subject: [PATCH 20/20] test: rename name field to test_name_field to check if n8n blocks fields named 'name' --- nodes/Bookstack/descriptions/Page.description.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index 112f815..3b8e785 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -53,8 +53,8 @@ export const pageFields: INodeProperties[] = [ description: 'Page title generated by the AI model (max 255 chars). A concise, descriptive title that summarizes the page content. If set, this overrides the Name field below. If both are empty, a title is auto-generated from the content.', }, { - displayName: 'Name', - name: 'name', + displayName: 'Test Name Field', + name: 'test_name_field', type: 'string', displayOptions: { show: { @@ -63,7 +63,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Page title for manual entry (max 255 chars). Only used if Page Title (AI) above is empty.', + description: 'TEST: Checking if renaming from "name" fixes the $fromAI button issue.', }, { displayName: 'Name',