Skill: tmigone/invoice-generator (version 1.0.0)

--- name: invoice-generator description: Generate professional PDF invoices from JSON data. Use when the user needs to create an invoice, billing document, or payment request with company/client details and line items. metadata: {"clawdbot":{"emoji":"🧾","requires":{"bins":["node","jq","weasyprint"],"env":["INVOICE_DIR"]},"primaryEnv":"INVOICE_DIR"}} --- # Invoice Generator Generate PDF invoices from structured JSON data. ## Setup 1. Install Node.js dependencies: ```bash cd invoice-generator && npm install ``` 2. Set `INVOICE_DIR` environment variable (or in `skills.entries.invoice-generator.env`): ```bash export INVOICE_DIR="/path/to/your/invoices" ``` This creates the directory structure: ``` $INVOICE_DIR/ ├── configs/ # Optional: saved invoice configs └── invoices/ # Generated PDF output ``` ## Usage ```bash # From stdin (on-the-fly) cat invoice-data.json | {baseDir}/scripts/generate.sh # From a full file path {baseDir}/scripts/generate.sh /path/to/invoice-data.json # From a saved config (looks in $INVOICE_DIR/configs/) {baseDir}/scripts/generate.sh client-template # Loads: $INVOICE_DIR/configs/client-template.json # Output goes to: $INVOICE_DIR/invoices/invoice-{number}.pdf (auto-versions if exists) ``` ## Input Data Format The JSON input must contain these fields: ```json { "company": { "name": "Your Company", "address": "123 Main St", "cityStateZip": "City, State, 12345", "country": "Country" }, "client": { "name": "Client Name", "address": "456 Client Ave", "cityStateZip": "City, State, 67890", "country": "Country", "taxId": "TAX123" }, "invoice": { "number": "INV-2025.01", "date": "Jan 15 2025", "dueDate": "Jan 30 2025" }, "items": [ { "description": "Service description", "rate": "1000.00", "currency": "USD" } ], "totals": { "currency": "USD", "total": "1,000.00" } } ``` See [references/data-schema.md](references/data-schema.md) for complete field documentation. ## Output The script outputs the path to the generated PDF file on success: ``` $INVOICE_DIR/invoices/invoice-INV-2025.01.pdf # If that filename already exists, the script will write: # $INVOICE_DIR/invoices/invoice-INV-2025.01-2.pdf (then -3, etc.) ``` ## Error Handling - Exits with code 1 if JSON is invalid or missing required fields - Exits with code 2 if weasyprint fails to generate PDF - Error messages are written to stderr

path: package-lock.json
content: {
  "name": "invoice-generator-skill",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "invoice-generator-skill",
      "version": "1.0.0",
      "dependencies": {
        "handlebars": "^4.7.8"
      }
    },
    "node_modules/handlebars": {
      "version": "4.7.8",
      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "minimist": "^1.2.5",
        "neo-async": "^2.6.2",
        "source-map": "^0.6.1",
        "wordwrap": "^1.0.0"
      },
      "bin": {
        "handlebars": "bin/handlebars"
      },
      "engines": {
        "node": ">=0.4.7"
      },
      "optionalDependencies": {
        "uglify-js": "^3.1.4"
      }
    },
    "node_modules/minimist": {
      "version": "1.2.8",
      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/neo-async": {
      "version": "2.6.2",
      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/source-map": {
      "version": "0.6.1",
      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/uglify-js": {
      "version": "3.19.3",
      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "optional": true,
      "bin": {
        "uglifyjs": "bin/uglifyjs"
      },
      "engines": {
        "node": ">=0.8.0"
      }
    },
    "node_modules/wordwrap": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    }
  }
}


path: package.json
content: {
  "name": "invoice-generator-skill",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "handlebars": "^4.7.8"
  }
}


path: references/data-schema.md
content: # Invoice Data Schema

Complete documentation of the JSON input format for invoice generation.

## Top-Level Structure

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `company` | object | Yes | Sender/biller information |
| `client` | object | Yes | Recipient/customer information |
| `invoice` | object | Yes | Invoice metadata |
| `items` | array | Yes | Line items on the invoice |
| `totals` | object | Yes | Summary totals |

## Company Object

Information about the entity issuing the invoice.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Company or individual name |
| `address` | string | Yes | Street address (can include newlines) |
| `cityStateZip` | string | Yes | City, state/province, and postal code |
| `country` | string | Yes | Country name |

## Client Object

Information about the invoice recipient.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Client company or individual name |
| `address` | string | Yes | Street address |
| `cityStateZip` | string | Yes | City, state/province, and postal code |
| `country` | string | Yes | Country name |
| `taxId` | string | Yes | Tax identification number |

## Invoice Object

Metadata about the invoice itself.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `number` | string | Yes | Unique invoice identifier (e.g., "INV-2025.01") |
| `date` | string | Yes | Invoice issue date (e.g., "Jan 15 2025") |
| `dueDate` | string | Yes | Payment due date |

## Items Array

Each item in the array represents a line item on the invoice.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `description` | string | Yes | Description of the service or product |
| `rate` | string | Yes | Price/rate as formatted string (e.g., "1500.00") |
| `currency` | string | Yes | Currency code (e.g., "USD", "EUR") |

## Totals Object

Summary financial information.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `currency` | string | Yes | Currency code for totals |
| `total` | string | Yes | Total amount as formatted string (e.g., "1,500.00") |

## Example

```json
{
  "company": {
    "name": "Acme Corp",
    "address": "123 Business Ave\nSuite 100",
    "cityStateZip": "San Francisco, CA 94102",
    "country": "United States"
  },
  "client": {
    "name": "Client Inc.",
    "address": "456 Customer St",
    "cityStateZip": "New York, NY 10001",
    "country": "United States",
    "taxId": "12-3456789"
  },
  "invoice": {
    "number": "INV-2025.01",
    "date": "Jan 15 2025",
    "dueDate": "Jan 30 2025"
  },
  "items": [
    {
      "description": "Consulting services - January 2025",
      "rate": "5000.00",
      "currency": "USD"
    },
    {
      "description": "Software license",
      "rate": "500.00",
      "currency": "USD"
    }
  ],
  "totals": {
    "currency": "USD",
    "total": "5,500.00"
  }
}
```


path: scripts/generate.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"

# Require INVOICE_DIR
: "${INVOICE_DIR:?Set INVOICE_DIR to the base directory for configs and invoices}"

# Set NODE_PATH so Node.js can find handlebars
export NODE_PATH="$SKILL_DIR/node_modules"

# Ensure directory structure exists
CONFIGS_DIR="$INVOICE_DIR/configs"
INVOICES_DIR="$INVOICE_DIR/invoices"
mkdir -p "$CONFIGS_DIR" "$INVOICES_DIR"

# Check for node_modules
if [[ ! -d "$SKILL_DIR/node_modules" ]]; then
    echo "Error: Dependencies not installed. Run 'npm install' in $SKILL_DIR" >&2
    exit 1
fi

# Read JSON from:
# 1. Full file path (if arg is a file)
# 2. Config name (looks in $CONFIGS_DIR/<name>.json)
# 3. stdin (if no arg)
if [[ $# -ge 1 ]]; then
    if [[ -f "$1" ]]; then
        # Full path provided
        JSON_DATA=$(cat "$1")
    elif [[ -f "$CONFIGS_DIR/$1.json" ]]; then
        # Config name provided (without .json)
        JSON_DATA=$(cat "$CONFIGS_DIR/$1.json")
    elif [[ -f "$CONFIGS_DIR/$1" ]]; then
        # Config name provided (with extension)
        JSON_DATA=$(cat "$CONFIGS_DIR/$1")
    else
        echo "Error: Config not found: $1 (looked in $CONFIGS_DIR)" >&2
        exit 1
    fi
else
    JSON_DATA=$(cat)
fi

# Validate JSON
if ! echo "$JSON_DATA" | jq empty 2>/dev/null; then
    echo "Error: Invalid JSON input" >&2
    exit 1
fi

# Check required fields
REQUIRED_FIELDS=(".company.name" ".client.name" ".invoice.number" ".items" ".totals.total")
for field in "${REQUIRED_FIELDS[@]}"; do
    if [[ $(echo "$JSON_DATA" | jq -r "$field") == "null" ]]; then
        echo "Error: Missing required field: $field" >&2
        exit 1
    fi
done

# Create temp directory for processing
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT

# Write JSON to temp file
echo "$JSON_DATA" > "$TEMP_DIR/data.json"

# Generate HTML using Node.js and Handlebars
node - "$SKILL_DIR/assets/invoice.hbs" "$TEMP_DIR/data.json" "$TEMP_DIR/out.html" << 'NODEJS'
const fs = require('fs');
const Handlebars = require('handlebars');

const [,, templatePath, dataPath, outputPath] = process.argv;

const template = fs.readFileSync(templatePath, 'utf8');
const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'));

const html = Handlebars.compile(template)(data);
fs.writeFileSync(outputPath, html);
NODEJS

if [[ $? -ne 0 ]]; then
    echo "Error: Failed to generate HTML" >&2
    exit 1
fi

# Extract invoice number for filename (sanitize for filesystem)
INVOICE_NUM=$(echo "$JSON_DATA" | jq -r '.invoice.number' | tr '/' '-' | tr ' ' '_')
OUTPUT_BASENAME="invoice-${INVOICE_NUM}"
OUTPUT_PDF="$INVOICES_DIR/${OUTPUT_BASENAME}.pdf"

# Auto-version if file already exists
if [[ -f "$OUTPUT_PDF" ]]; then
    i=2
    while true; do
        candidate="$INVOICES_DIR/${OUTPUT_BASENAME}-${i}.pdf"
        if [[ ! -f "$candidate" ]]; then
            OUTPUT_PDF="$candidate"
            break
        fi
        i=$((i + 1))
    done
fi

# Convert HTML to PDF using weasyprint
if ! weasyprint "$TEMP_DIR/out.html" "$OUTPUT_PDF" 2>/dev/null; then
    echo "Error: Failed to generate PDF (is weasyprint installed?)" >&2
    exit 2
fi

# Output the path to the generated PDF
echo "$OUTPUT_PDF"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)