Agent skill

PDF Generation, Forms and Extraction

by pipeshub-ai in pipeshub-ai/pipeshub-ai

Picks the right library for generating a new PDF, filling an existing PDF form, or extracting text and tables, defaulting to Node where possible.

Apache-2.0Auto-check passedDocuments & Office

Install PDF Generation, Forms and Extraction

skills CLI
$ npx skills add pipeshub-ai/pipeshub-ai --skill pdf -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install pipeshub-ai/pipeshub-ai pdf --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/pipeshub-ai/pipeshub-ai.git skills-src && mkdir -p .claude/skills && cp -r skills-src/backend/python/app/agents/agent_loop/skills/builtin_packs/pdf .claude/skills/pdf && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
pdf
GitHub stars
3.8k
Token cost
~2.9k tokens
SKILL.md length
766 words
Files
1
Skills in repo
9
Repo updated
First seen
Licence
Apache-2.0

At a glance

Picks the right library for generating a new PDF, filling an existing PDF form, or extracting text and tables, defaulting to Node where possible.

  • Works in 5 steps: Switch to landscape when column count ≥ 6 → Compute column widths from available… → Reduce font size to 8–9pt for dense… → …
  • Generating a new PDF report or invoice from scratch
  • SKILL.md covers Quick Reference, Generating a new PDF (pdfkit), Filling in a form (pdf-lib) and Extracting content (Python —…, plus 1 more section
  • Instructions only: no scripts, shell commands, URLs or credentials in SKILL.md

What it does

This skill is a decision table for PDF work: generate a new PDF from scratch with a TypeScript library by default, or a Python reporting library when a report needs several pages of tabular data and automatic pagination beats hand-rolled page breaks; fill an existing form with a TypeScript or Python library; and extract text or tables from an existing PDF in Python, since there's no solid Node option for extraction.

Because the default generation library has no built-in table primitive, it walks through drawing tabular content manually - fixed per-column x-offsets, a page-break check before every row, and computing each row's height from the tallest wrapped cell rather than using a fixed height, which otherwise causes overlapping rows. It also covers setting an explicit page size rather than relying on a locale-dependent default.

It explicitly excludes converting an existing Word, PowerPoint or Excel file to PDF, since that needs LibreOffice and is handled by a separate skill; the libraries it calls for are already installed or allowlisted in its environment.

When your agent uses it

  • Generating a new PDF report or invoice from scratch
  • Filling in the fields of an existing PDF form
  • Extracting text or tables out of an existing PDF

Example prompts

  • “Generate an invoice PDF with a line-item table from this order data.”
  • “Fill in the W-9 form in ./forms/w9.pdf with my business details.”
  • “Extract the tables from this scanned report PDF.”

Requirements

  • A TypeScript or Python PDF library such as pdf-lib or pypdf

Workflow steps

5 steps, taken from the first numbered list in SKILL.md.

  1. Switch to landscape when column count ≥ 6
  2. Compute column widths from available page width, never hardcode pixel values
  3. Reduce font size to 8–9pt for dense tables (many rows and columns); header can stay 9–10pt bold.
  4. Truncate long cell text rather than letting it overflow into the next column
  5. Wrap within cells as a fallback — pass { width: colWidth } and compute row height with doc.heightOfString(cellText, { width: colWidth })…

What it can do on your machine

Read from SKILL.md and the folder at commit a883af6. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md (its code samples are typescript and python).

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

PDF Generation, Forms and Extraction loads about 2.9k tokens when it runs. Until then it costs about 89 tokens; SKILL.md has 766 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~89
When it runs · the whole SKILL.md, loaded when a task matches
~2.9k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from pipeshub-ai/pipeshub-ai at commit a883af6, republished under its Apache-2.0 licence (© pipeshub-ai). 766 words, ~2,894 tokens.

Download SKILL.mdSave it as .claude/skills/pdf/SKILL.md (or your agent's skills folder).
name
pdf
description
Use this skill whenever the user asks for a PDF — generating a new one from scratch (reports, invoices, letters), extracting text/tables from an existing PDF, or filling in an existing PDF form. Does not cover converting an existing Word/PowerPoint/Excel file to PDF (that requires LibreOffice, not available in this environment — see file-conversion).

PDF documents

Quick Reference

TaskDefault (Node)Fallback (Python)
Generate a new PDF from scratchpdfkit — write a TypeScript program (packages: ["pdfkit"]) and run it via your coding tool; pre-installed in the standard sandbox imagereportlab's platypus — reach for this for a report with several full pages of tabular data, where platypus.Table's built-in pagination beats hand-rolling page breaks in pdfkit (see below)
Fill in an existing PDF form (AcroForm)pdf-lib — TypeScript program (packages: ["pdf-lib"], pre-installed); form.getTextField(name).setText(value)pypdf (PdfWriter.update_page_form_field_values)
Extract text from an existing PDF— (no good Node equivalent)pdfplumber (also gets tables) or pypdf (faster, text-only)
Extract tables from an existing PDF— (no good Node equivalent)pdfplumber (page.extract_tables())

All of the above are already installed/allowlisted — no extra setup either way. Default to pdfkit/pdf-lib in TypeScript for generation and form-filling per this environment's Node-first policy for office-document creation; extraction has no solid Node option here, so stays Python.

Generating a new PDF (pdfkit)

pdfkit is a low-level, stream-based PDF library — it does not have reportlab.platypus's automatic page-flow layout for tables, so you place text yourself and check the vertical position before every element:

typescript
import PDFDocument from "pdfkit";
import fs from "fs";

const doc = new PDFDocument({ size: "A4", margins: { top: 50, bottom: 50, left: 50, right: 50 } });
doc.pipe(fs.createWriteStream("report.pdf"));

doc.fontSize(18).text("Invoice", { align: "center" });
doc.moveDown();

// doc.text() without an explicit x/y auto-wraps and flows below the previous
// element, including inserting new pages for long paragraphs — only switch
// to explicit x/y positioning (below) for tabular/columnar layout.
doc.fontSize(11).text("Body text wraps and paginates automatically here.");

doc.end();
  • Always set size explicitly ("A4" or "LETTER") rather than relying on the default, since it may not match the user's locale expectation.
  • Plain flowing text (paragraphs, letters) needs no manual layout — doc.text() alone handles wrapping and page breaks. Only tabular/columnar content needs the manual pattern below.
  • For tabular data (an invoice's line items, a report's data table), pdfkit has no built-in table primitive — draw it with fixed per-column x offsets and a manual page-break check before each row:
typescript
const startX = 50;
const colWidths = { desc: 260, qty: 60, total: 90 };
let y = doc.y;

for (const item of items) {
  if (y > doc.page.height - doc.page.margins.bottom - 20) {
    doc.addPage();
    y = doc.page.margins.top;
  }
  doc.text(item.desc, startX, y, { width: colWidths.desc });
  doc.text(String(item.qty), startX + colWidths.desc, y, { width: colWidths.qty, align: "right" });
  doc.text(item.total.toFixed(2), startX + colWidths.desc + colWidths.qty, y, { width: colWidths.total, align: "right" });
  // Compute row height from the tallest cell; fixed values cause overlapping when a cell wraps.
  const rowHeight = Math.max(
    doc.heightOfString(item.desc, { width: colWidths.desc }),
    doc.heightOfString(String(item.qty), { width: colWidths.qty }),
    doc.heightOfString(item.total.toFixed(2), { width: colWidths.total }),
  ) + 4; // 4pt bottom padding
  y += rowHeight;
}
  • Always compute row height from doc.heightOfString(text, { width }) across all cells in the row and use the maximum. A fixed row height causes rows to overlap whenever any cell wraps to a second line.
  • Reach for reportlab.platypus.Table (Python) instead when the table is long enough that hand-rolling pagination is more work than it's worth — it paginates a table across pages automatically given column widths and a TableStyle.
Wide tables (6+ columns) — preventing overflow

Tables with many columns (ticket lists, dashboards, audit logs) overflow if you use portrait orientation or eyeball column widths. Follow this checklist:

  1. Switch to landscape when column count ≥ 6:
    typescript
    const doc = new PDFDocument({ size: "A4", layout: "landscape", margins: { top: 40, bottom: 40, left: 40, right: 40 } });
  2. Compute column widths from available page width, never hardcode pixel values:
    typescript
    const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
    // Give variable-length columns (e.g. "Summary") a proportionally larger share
    const fixedColWidth = 70; // narrow cols: status, priority, type, etc.
    const fixedCols = 6;
    const flexColWidth = pageWidth - fixedCols * fixedColWidth; // remainder for "Summary"
  3. Reduce font size to 8–9pt for dense tables (many rows and columns); header can stay 9–10pt bold.
  4. Truncate long cell text rather than letting it overflow into the next column:
    typescript
    function truncate(original: string, font: string, size: number, maxWidth: number): string {
      let text = original;
      while (doc.widthOfString(text, { font, size }) > maxWidth && text.length > 0) {
        text = text.slice(0, -1);
      }
      return text.length < original.length ? text + "…" : text;
    }
    Or use { width: colWidth, ellipsis: true } in the .text() call (pdfkit supports this natively).
  5. Wrap within cells as a fallback — pass { width: colWidth } and compute row height with doc.heightOfString(cellText, { width: colWidth }) across all cells in the row, then use the tallest.
Show full SKILL.md (318 more words)Show less
Wide tables — reportlab (Python, preferred for 20+ row tables)

reportlab.platypus.Table handles both pagination and cell wrapping automatically when you give it explicit colWidths that sum to the available page width:

python
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT

pagesize = landscape(A4)
doc = SimpleDocTemplate("report.pdf", pagesize=pagesize,
                        topMargin=15*mm, bottomMargin=15*mm,
                        leftMargin=15*mm, rightMargin=15*mm)
available_width = pagesize[0] - 30*mm  # left + right margins

# Assign proportional widths — flex column gets remainder
fixed = {"key": 45*mm, "type": 30*mm, "status": 30*mm,
         "priority": 28*mm, "due": 28*mm, "sprint": 35*mm}
flex_width = available_width - sum(fixed.values())  # "Summary" gets the rest

col_widths = [fixed["key"], flex_width, fixed["type"], fixed["status"],
              fixed["priority"], fixed["due"], fixed["sprint"]]

# Every cell must be a Paragraph — plain strings in Table cells do NOT wrap
styles = getSampleStyleSheet()
cell_style = ParagraphStyle("Cell", parent=styles["Normal"], fontSize=8, leading=10,
                            splitLongWords=True, wordWrap="CJK")
header_style = ParagraphStyle("Header", parent=cell_style, fontSize=9,
                              textColor=colors.white, fontName="Helvetica-Bold")

def P(text, style=cell_style):
    return Paragraph(str(text) if text is not None else "—", style)

table_data = [[P(h, header_style) for h in ["Key", "Summary", "Type", "Status", "Priority", "Due", "Sprint"]]]
for ticket in tickets:
    table_data.append([
        P(ticket["key"]),
        P(ticket["summary"]),
        P(ticket["type"]),
        P(ticket["status"]),
        P(ticket["priority"]),
        P(ticket.get("due", "—")),
        P(ticket.get("sprint", "—")),
    ])

table = Table(table_data, colWidths=col_widths, repeatRows=1)
table.setStyle(TableStyle([
    ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#2c3e50")),
    # TEXTCOLOR/FONTNAME/FONTSIZE for header are now carried by header_style Paragraph
    ("FONTSIZE", (0, 1), (-1, -1), 8),
    ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.Color(0.95, 0.95, 0.95)]),
    ("GRID", (0, 0), (-1, -1), 0.4, colors.grey),
    ("VALIGN", (0, 0), (-1, -1), "TOP"),
    ("TOPPADDING", (0, 0), (-1, -1), 4),
    ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
    ("LEFTPADDING", (0, 0), (-1, -1), 4),
    ("RIGHTPADDING", (0, 0), (-1, -1), 4),
]))
doc.build([table])

Key points:

  • landscape(A4) — always use landscape for 6+ column tables.
  • colWidths must sum to available_width — if they exceed it, the table overflows the page; if they're omitted, reportlab auto-sizes but often overflows.
  • Wrap every cell in Paragraph(..., style) — including headers and short cells. Plain strings inside a Table cell never wrap; they clip or overflow regardless of colWidths. This is the #1 cause of reportlab table overflow. Use a helper P(text, style=cell_style) to keep the data-building code readable. Add splitLongWords=True, wordWrap="CJK" to cell_style so URLs and other long unbreakable tokens are force-split rather than overflowing.
  • repeatRows=1 — repeats the header row on every page when the table spans multiple pages.
  • Font size 8–9pt for body, 9–10pt for header — standard for dense reports.

Filling in a form (pdf-lib)

typescript
import { PDFDocument } from "pdf-lib";
import fs from "fs";

const pdfDoc = await PDFDocument.load(fs.readFileSync("form.pdf"));
const form = pdfDoc.getForm();
for (const field of form.getFields()) console.log(field.getName(), field.constructor.name); // discover field names/types first

form.getTextField("first_name").setText("Jane");
fs.writeFileSync("filled.pdf", await pdfDoc.save());

Field names are exactly what the form's original author named them (often generic like Text1) — always call form.getFields() first rather than guessing. pdf-lib is also the right tool for merging/splitting existing PDFs (PDFDocument.copyPages) if that comes up alongside form-filling.

Extracting content (Python — no solid Node equivalent for this)

  • pdfplumber: page.extract_text() for reading order-preserving text, page.extract_tables() for tabular data — the better default for anything that might contain a table, since pypdf's text extraction does not attempt to reconstruct table structure.
  • pypdf: faster and sufficient when you only need plain text and know the source has no tables (e.g. extracting a paragraph from a text-heavy report).
  • Scanned (image-only) PDFs have no extractable text via either library — check page.extract_text() for an empty/near-empty result and tell the user OCR would be needed (not available in this environment) rather than returning an empty string silently.

Not supported in this environment

  • Converting an existing .docx/.pptx/.xlsx to PDF requires a rendering engine (LibreOffice) not installed in the sandbox yet.

© pipeshub-ai, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in backend/python/app/agents/agent_loop/skills/builtin_packs/pdf of pipeshub-ai/pipeshub-ai.

Open the folder on GitHubat commit a883af6

Compare with similar skills

PDF Generation, Forms and Extraction next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

PDF Generation, Forms and Extraction compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
PDF Generation, Forms and Extraction this skillpipeshub-ai/pipeshub-ai3.8k—~2.9kAutomated safety check: PassApache-2.0
PDF ToolkitXiaomiMiMo/MiMo-Code14k—~1.7kAutomated safety check: PassApache-2.0
React PDFtrailofbits/skills-curated513—~3.1kAutomated safety check: NotesCC-BY-SA-4.0
PDF Processinganthropics/skills180k48 repos~2kAutomated safety check: PassProprietary
PDF Processing with PythonHKUDS/DeepTutor41k—~2.7kAutomated safety check: PassApache-2.0
PDF ToolkitTokenRhythm/opensquilla7.1k—~1.9kAutomated safety check: PassApache-2.0

Similar skills

  • PDF Toolkit

    XiaomiMiMo/MiMo-Code

    Reads, transforms, composes and fills PDFs with Python scripts for extraction, merging, watermarking, encryption, OCR and form filling.

    14k GitHub stars~1.7k tokensUpdated yesterday
    Documents & OfficeAuto-check passed
  • React PDF

    trailofbits/skills-curated

    Official

    Generates PDF documents using the React-PDF library (@react-pdf/renderer) with TypeScript and JSX.

    513 GitHub stars~3.1k tokensUpdated 2 mo ago
    Documents & OfficeAuto-check: notes
  • PDF Processing

    anthropics/skills

    Official

    Handles everyday PDF jobs in Python and on the command line: extract text and tables, merge, split, rotate, watermark, fill forms, encrypt and OCR.

    180k GitHub starsUsed in 48 repos~2k tokens
    Documents & OfficeAuto-check passed
  • Reads, extracts from, creates, merges, splits, watermarks, encrypts and fills PDF files with pdfplumber, pypdf and reportlab inside a Python sandbox.

    41k GitHub stars~2.7k tokensUpdated 2 days ago
    Documents & OfficeAuto-check passed
  • PDF Toolkit

    TokenRhythm/opensquilla

    Deterministic PDF operations through bundled scripts: extract text and tables, merge files or page ranges, split by range, fill form fields and build PDFs from data.

    7.1k GitHub stars~1.9k tokensUpdated today
    Documents & OfficeAuto-check passed
  • PDF Processing Guide

    agentscope-ai/QwenPaw

    Handles PDF tasks with Python libraries and command-line tools: extract text and tables, merge, split, rotate, create, fill forms and more.

    36k GitHub stars~1.8k tokensUpdated yesterday
    Documents & OfficeAuto-check passed

More from pipeshub-ai/pipeshub-ai

All 9 skills in this repo
  • PipesHub Excel Spreadsheet Builder

    pipeshub-ai/pipeshub-ai

    Creates and edits .xlsx workbooks with real Excel formulas rather than hardcoded computed values, defaulting to exceljs in TypeScript with a static formula-safety check.

    3.8k GitHub stars~1.8k tokensUpdated yesterday
    Auto-check passed
  • Office Open XML Utilities

    pipeshub-ai/pipeshub-ai

    Unpacks a .docx or .pptx into pretty-printed XML, lets you make small targeted edits, and repacks it into a file Office will open.

    3.8k GitHub stars~1.1k tokensUpdated yesterday
    Auto-check passed
  • PowerPoint Deck Builder

    pipeshub-ai/pipeshub-ai

    Creates new PowerPoint decks with pptxgenjs in TypeScript, reads existing decks with python-pptx, and applies a design-quality checklist so every slide has real visual hierarchy.

    3.8k GitHub stars~1.6k tokensUpdated yesterday
    Auto-check passed
  • Verified Data Analysis with pandas

    pipeshub-ai/pipeshub-ai

    Loads, cleans, aggregates and joins tabular data with pandas under a verification rule: every number reported must be one that the code actually printed.

    3.8k GitHub stars~1.2k tokensUpdated yesterday
    Auto-check passed
  • Chart Type Selection Guide

    pipeshub-ai/pipeshub-ai

    Picks the right chart type for a data question and applies readability rules like axis labels, colorblind palettes and legend restraint.

    3.8k GitHub stars~1.1k tokensUpdated yesterday
    Auto-check passed
  • Word Document Creation And Editing

    pipeshub-ai/pipeshub-ai

    Routes a Word document request to the right approach: a TypeScript library for new files, XML editing for existing ones, and plain reading only.

    3.8k GitHub stars~1.3k tokensUpdated yesterday
    Auto-check passed

Questions about PDF Generation, Forms and Extraction

What does PDF Generation, Forms and Extraction do?

Picks the right library for generating a new PDF, filling an existing PDF form, or extracting text and tables, defaulting to Node where possible. This skill is a decision table for PDF work: generate a new PDF from scratch with a TypeScript library by default, or a Python reporting library when a report needs several pages of tabular data and automatic pagination beats hand-rolled page breaks; fill an existing form with a TypeScript or Python library; and extract text or tables from an existing PDF in Python, since there's no solid Node option for extraction.

When should I use PDF Generation, Forms and Extraction?

PDF Generation, Forms and Extraction fits situations like: generating a new PDF report or invoice from scratch; filling in the fields of an existing PDF form; extracting text or tables out of an existing PDF.

How do I install PDF Generation, Forms and Extraction in Claude Code?

Run `npx skills add pipeshub-ai/pipeshub-ai --skill pdf -a claude-code`. Or copy the skill folder (backend/python/app/agents/agent_loop/skills/builtin_packs/pdf in pipeshub-ai/pipeshub-ai) into .claude/skills/pdf in your project. Claude Code loads it when a task matches its description.

How do I install PDF Generation, Forms and Extraction in Codex?

Run `npx skills add pipeshub-ai/pipeshub-ai --skill pdf -a codex`. Or copy the skill folder (backend/python/app/agents/agent_loop/skills/builtin_packs/pdf in pipeshub-ai/pipeshub-ai) into .agents/skills/pdf in your project. Codex loads it when a task matches its description.

Can I use PDF Generation, Forms and Extraction in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add pipeshub-ai/pipeshub-ai --skill pdf -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/pdf, .gemini/skills/pdf, .github/skills/pdf and .opencode/skills/pdf in your project.

What does PDF Generation, Forms and Extraction need to run?

SKILL.md names no scripts, command-line tools or credentials: PDF Generation, Forms and Extraction is instructions for the agent only. Our summary lists: A TypeScript or Python PDF library such as pdf-lib or pypdf.

Does PDF Generation, Forms and Extraction access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is PDF Generation, Forms and Extraction safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does PDF Generation, Forms and Extraction use?

PDF Generation, Forms and Extraction is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does PDF Generation, Forms and Extraction use?

About 2.9k tokens (SKILL.md is roughly 12k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to PDF Generation, Forms and Extraction?

Skills that share tags, products or a category with PDF Generation, Forms and Extraction: PDF Toolkit (XiaomiMiMo/MiMo-Code, 14k stars), React PDF (trailofbits/skills-curated, 513 stars), PDF Processing (anthropics/skills, 180k stars) and PDF Processing with Python (HKUDS/DeepTutor, 41k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains PDF Generation, Forms and Extraction?

pipeshub-ai (a GitHub organization) maintains it in pipeshub-ai/pipeshub-ai, which has 3,821 GitHub stars. The repository holds 9 skills in this directory. The repository was last updated on October 9, 2026.

Source: pipeshub-ai/pipeshub-ai on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.