Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 329 additions & 0 deletions .agent/scripts/pdf-helper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
#!/usr/bin/env bash
# pdf-helper.sh - PDF operations helper using LibPDF
# Usage: pdf-helper.sh [command] [options]
#
# Commands:
# info <file> - Show PDF information (pages, form fields, etc.)
# fields <file> - List form field names and types
# fill <file> <json> - Fill form fields from JSON
# merge <output> <files..> - Merge multiple PDFs
# extract <file> <pages> - Extract pages (e.g., "1-3,5,7-9")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header comment advertises an extract command, but there’s no extract implementation (and it’s not listed in cmd_help / main()), so users will hit “Unknown command”. Consider removing it from the header or adding the command handler.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

# text <file> - Extract text content
# install - Install @libpdf/core
# help - Show this help

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Check if bun or node is available
get_runtime() {
if command -v bun &>/dev/null; then
echo "bun"
elif command -v node &>/dev/null; then
echo "node"
else
echo ""
fi
}

# Check if @libpdf/core is installed
check_libpdf() {
local runtime
runtime=$(get_runtime)

if [[ -z "$runtime" ]]; then
echo -e "${RED}Error:${NC} Neither bun nor node found. Install one first."
return 1
fi

# Check in current project or global
if [[ -f "package.json" ]] && grep -q "@libpdf/core" package.json 2>/dev/null; then
return 0
fi

# Try to import
if $runtime -e "require('@libpdf/core')" 2>/dev/null; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check_libpdf uses require('@libpdf/core') to test availability; since @libpdf/core is ESM (type: "module"), this will fail under Node and can falsely report “not found” (especially for non-local installs). Consider checking via ESM import() (matching run_script’s module mode).

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

return 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The check for @libpdf/core uses require(), which will fail in a Node.js ESM context. Since run_script uses import with --input-type=module, this check should also use import to be consistent and correct.

Suggested change
if $runtime -e "require('@libpdf/core')" 2>/dev/null; then
return 0
fi
if [[ "$runtime" == "bun" ]] && bun -e "import('@libpdf/core')" &>/dev/null; then
return 0
elif [[ "$runtime" == "node" ]] && node --input-type=module -e "import('@libpdf/core')" &>/dev/null; then
return 0
fi


echo -e "${YELLOW}Warning:${NC} @libpdf/core not found."
echo -e "Install with: ${BLUE}npm install @libpdf/core${NC} or ${BLUE}bun add @libpdf/core${NC}"
return 1
}

# Run TypeScript/JavaScript code
run_script() {
local script="$1"
local runtime
runtime=$(get_runtime)

if [[ "$runtime" == "bun" ]]; then
bun -e "$script"
else
node --input-type=module -e "$script"
fi
}

# Show PDF info
cmd_info() {
local file="$1"

if [[ ! -f "$file" ]]; then
echo -e "${RED}Error:${NC} File not found: $file"
return 1
fi

check_libpdf || return 1

run_script "
import { PDF } from '@libpdf/core';
import { readFileSync } from 'fs';

const bytes = readFileSync('$file');
const pdf = await PDF.load(bytes);
const pages = await pdf.getPages();
const form = await pdf.getForm();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LibPDF pdf.getForm() returns PDFForm | null; form.getFieldNames() will throw for PDFs without an AcroForm. Consider guarding for null/empty forms here (and in other commands that call getForm()).

Other Locations
  • .agent/scripts/pdf-helper.sh:122
  • .agent/scripts/pdf-helper.sh:157

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

const fields = form.getFieldNames();

console.log('File:', '$file');
console.log('Pages:', pages.length);
console.log('Form fields:', fields.length);

if (pages.length > 0) {
const { width, height } = pages[0].getSize();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pages[0].getSize() doesn’t match the LibPDF PDFPage API (which exposes page.width/page.height), so info may crash on valid PDFs. Consider switching to the supported page dimension properties.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

console.log('Page size:', Math.round(width), 'x', Math.round(height), 'points');
}
"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This block is vulnerable to command injection. The $file variable is directly interpolated into the JavaScript string. A malicious filename containing special characters (e.g., a single quote) could break out of the string and execute arbitrary code. This is a critical security risk.

To fix this, you should pass shell variables to the script via environment variables and use single quotes for the script string to prevent shell expansion.

This same vulnerability pattern exists in cmd_fields, cmd_fill, cmd_merge, and cmd_text and must be fixed in all of them.

Here is the corrected version for cmd_info:

Suggested change
run_script "
import { PDF } from '@libpdf/core';
import { readFileSync } from 'fs';
const bytes = readFileSync('$file');
const pdf = await PDF.load(bytes);
const pages = await pdf.getPages();
const form = await pdf.getForm();
const fields = form.getFieldNames();
console.log('File:', '$file');
console.log('Pages:', pages.length);
console.log('Form fields:', fields.length);
if (pages.length > 0) {
const { width, height } = pages[0].getSize();
console.log('Page size:', Math.round(width), 'x', Math.round(height), 'points');
}
"
FILE="$file" run_script '
import { PDF } from "@libpdf/core";
import { readFileSync } from "fs";
const file = process.env.FILE;
const bytes = readFileSync(file);
const pdf = await PDF.load(bytes);
const pages = await pdf.getPages();
const form = await pdf.getForm();
const fields = form.getFieldNames();
console.log("File:", file);
console.log("Pages:", pages.length);
console.log("Form fields:", fields.length);
if (pages.length > 0) {
const { width, height } = pages[0].getSize();
console.log("Page size:", Math.round(width), "x", Math.round(height), "points");
}
'

}

# List form fields
cmd_fields() {
local file="$1"

if [[ ! -f "$file" ]]; then
echo -e "${RED}Error:${NC} File not found: $file"
return 1
fi

check_libpdf || return 1

run_script "
import { PDF } from '@libpdf/core';
import { readFileSync } from 'fs';

const bytes = readFileSync('$file');
const pdf = await PDF.load(bytes);
const form = await pdf.getForm();
const fields = form.getFields();

if (fields.length === 0) {
console.log('No form fields found.');
} else {
console.log('Form fields:');
for (const field of fields) {
const name = field.getName();
const type = field.constructor.name.replace('PDF', '').replace('Field', '');
console.log(' -', name, '(' + type + ')');
}
}
"
}

# Fill form fields
cmd_fill() {
local file="$1"
local json="$2"
local output="${3:-${file%.pdf}-filled.pdf}"

if [[ ! -f "$file" ]]; then
echo -e "${RED}Error:${NC} File not found: $file"
return 1
fi

check_libpdf || return 1

run_script "
import { PDF } from '@libpdf/core';
import { readFileSync, writeFileSync } from 'fs';

const bytes = readFileSync('$file');
const pdf = await PDF.load(bytes);
const form = await pdf.getForm();

const data = JSON.parse('$json');
form.fill(data);

const output = await pdf.save();
writeFileSync('$output', output);
console.log('Filled PDF saved to:', '$output');
"
}

# Merge PDFs
cmd_merge() {
local output="$1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The merge command depends on jq to construct the JSON array of files, but the script doesn't check if jq is installed. This will cause the script to fail abruptly if jq is missing. You should add a check and provide a user-friendly error message.

Suggested change
local output="$1"
if ! command -v jq &>/dev/null; then
echo -e "${RED}Error:${NC} 'jq' is not installed. Please install it to use the merge command."
return 1
fi
local output="$1"

shift
local files=("$@")

if [[ ${#files[@]} -lt 2 ]]; then
echo -e "${RED}Error:${NC} Need at least 2 files to merge"
return 1
fi

check_libpdf || return 1

local files_json
files_json=$(printf '%s\n' "${files[@]}" | jq -R . | jq -s .)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd_merge depends on jq, but the script doesn’t check for it; with set -e, a missing jq will abort with a confusing error. Consider adding a preflight check with a clearer message.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎


run_script "
import { PDF } from '@libpdf/core';
import { readFileSync, writeFileSync } from 'fs';

const files = $files_json;
const pdfs = files.map(f => readFileSync(f));

const merged = await PDF.merge(pdfs);
const output = await merged.save();
writeFileSync('$output', output);
console.log('Merged', files.length, 'PDFs into:', '$output');
"
}

# Extract text
cmd_text() {
local file="$1"

if [[ ! -f "$file" ]]; then
echo -e "${RED}Error:${NC} File not found: $file"
return 1
fi

check_libpdf || return 1

run_script "
import { PDF } from '@libpdf/core';
import { readFileSync } from 'fs';

const bytes = readFileSync('$file');
const pdf = await PDF.load(bytes);
const pages = await pdf.getPages();

for (let i = 0; i < pages.length; i++) {
const text = await pages[i].getTextContent();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LibPDF PDFPage exposes extractText(); getTextContent() is likely not a method, so text may fail at runtime. Consider aligning this with the LibPDF API used elsewhere.

Other Locations
  • .agent/tools/pdf/libpdf.md:346

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

if (pages.length > 1) {
console.log('--- Page', i + 1, '---');
}
console.log(text);
}
"
}

# Install @libpdf/core
cmd_install() {
local runtime
runtime=$(get_runtime)

if [[ -z "$runtime" ]]; then
echo -e "${RED}Error:${NC} Neither bun nor node found. Install one first."
return 1
fi

echo -e "${BLUE}Installing @libpdf/core...${NC}"

if [[ "$runtime" == "bun" ]]; then
bun add @libpdf/core
else
npm install @libpdf/core
fi

echo -e "${GREEN}Done!${NC}"
}

# Show help
cmd_help() {
cat << 'EOF'
pdf-helper.sh - PDF operations helper using LibPDF

Usage: pdf-helper.sh [command] [options]

Commands:
info <file> - Show PDF information (pages, form fields, etc.)
fields <file> - List form field names and types
fill <file> <json> [out] - Fill form fields from JSON
merge <output> <files..> - Merge multiple PDFs
text <file> - Extract text content
install - Install @libpdf/core
help - Show this help

Examples:
# Show PDF info
pdf-helper.sh info document.pdf

# List form fields
pdf-helper.sh fields form.pdf

# Fill form fields
pdf-helper.sh fill form.pdf '{"name":"John","email":"john@example.com"}'

# Merge PDFs
pdf-helper.sh merge combined.pdf doc1.pdf doc2.pdf doc3.pdf

# Extract text
pdf-helper.sh text document.pdf

Requirements:
- Node.js 20+ or Bun
- @libpdf/core (install with: npm install @libpdf/core)

For more advanced operations (signing, encryption, etc.), use LibPDF directly
in your TypeScript/JavaScript code. See: https://libpdf.dev
EOF
}

# Main
main() {
local cmd="${1:-help}"
shift || true

case "$cmd" in
info)
[[ $# -lt 1 ]] && { echo -e "${RED}Error:${NC} Missing file argument"; return 1; }
cmd_info "$1"
;;
fields)
[[ $# -lt 1 ]] && { echo -e "${RED}Error:${NC} Missing file argument"; return 1; }
cmd_fields "$1"
;;
fill)
[[ $# -lt 2 ]] && { echo -e "${RED}Error:${NC} Missing file or json argument"; return 1; }
cmd_fill "$@"
;;
merge)
[[ $# -lt 3 ]] && { echo -e "${RED}Error:${NC} Need output file and at least 2 input files"; return 1; }
cmd_merge "$@"
;;
text)
[[ $# -lt 1 ]] && { echo -e "${RED}Error:${NC} Missing file argument"; return 1; }
cmd_text "$1"
;;
install)
cmd_install
;;
help|--help|-h)
cmd_help
;;
*)
echo -e "${RED}Error:${NC} Unknown command: $cmd"
echo "Run 'pdf-helper.sh help' for usage"
return 1
;;
esac
}

main "$@"
6 changes: 4 additions & 2 deletions .agent/subagent-index.toon
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ flash,gemini-2.5-flash,Fast cheap large context
pro,gemini-2.5-pro,Capable large context
-->

<!--TOON:subagents[32]{folder,purpose,key_files}:
<!--TOON:subagents[33]{folder,purpose,key_files}:
aidevops/,Framework internals - extending aidevops and architecture,setup|architecture|troubleshooting
memory/,Cross-session memory - SQLite FTS5 storage,README
seo/,Search optimization - keywords and rankings,dataforseo|serper|google-search-console|gsc-sitemaps|site-crawler|eeat-score
Expand All @@ -35,6 +35,7 @@ tools/ai-assistants/,AI tool integration - configuring assistants,agno|capsolver
tools/ai-orchestration/,AI orchestration - visual builders and multi-agent,overview|langflow|crewai|autogen|openprose
tools/browser/,Browser automation - scraping and testing,agent-browser|stagehand|playwright|playwriter|crawl4ai|pagespeed|peekaboo
tools/mobile/,Mobile development - iOS/Android emulators,minisim
tools/pdf/,PDF processing - form filling and digital signatures,overview|libpdf
tools/ui/,UI components - design systems and debugging,shadcn|ui-skills|frontend-debugging
tools/code-review/,Code quality - linting and security scanning,code-standards|code-simplifier|codacy|coderabbit|qlty|snyk|secretlint
tools/context/,Context optimization - semantic search and indexing,osgrep|augment-context-engine|context-builder|context7|toon|mcp-discovery
Expand Down Expand Up @@ -72,7 +73,7 @@ bug-fixing,workflows/bug-fixing.md,Bug fix workflow
feature-development,workflows/feature-development.md,Feature development workflow
-->

<!--TOON:scripts[21]{name,purpose}:
<!--TOON:scripts[22]{name,purpose}:
list-keys-helper.sh,List all API keys with storage locations
linters-local.sh,Run local linting (ShellCheck secretlint patterns)
code-audit-helper.sh,Run remote auditing (CodeRabbit Codacy SonarCloud)
Expand All @@ -93,5 +94,6 @@ full-loop-helper.sh,End-to-end development loop (task to PR to deploy)
session-review-helper.sh,Gather session context for review
mail-helper.sh,TOON-based inter-agent mailbox system
memory-helper.sh,Cross-session memory (SQLite FTS5)
pdf-helper.sh,PDF operations (info fields fill merge text)
unstract-helper.sh,Unstract self-hosted document processing (install start stop)
-->
Loading
Loading