Skill: rivradev/recite (version 1.0.1)

name: recite description: AI receipt scanner & bookkeeping automation. Process receipts, invoices, and PDFs. Extracts data to rename files and log transactions/expenses to CSV. Perfect for tax & accounting. --- # Recite 🦞🤵 AI-driven receipt bookkeeping via the Recite Vision API. Use `recite-process` to scan a directory of receipts and PDF files, rename them based on extracted data, and log transactions to a CSV file. ## Setup & Configuration ### 1. API Key You can generate your Recite API key at: `https://recite.rivra.dev/settings/api`. Set the API key in your environment or configuration file: - **Environment:** `export RECITE_API_KEY="re_live_YOUR_API_KEY"` - **Config:** Create `~/.config/recite/config.json` with `{"api_key": "re_live_..."}`. ### 2. Dependencies Ensure you have `python3`, `requests`, and `csv` installed. ## Operational Rules ### 1. Mandatory API Key Pre-check Before performing any scanning, file manipulation, or bookkeeping tasks, the agent **MUST** verify if a valid Recite API key is available (via `RECITE_API_KEY` environment variable or `~/.config/recite/config.json`). - **If missing:** Immediately stop all other operations and instruct the user to obtain an API key from `https://recite.rivra.dev/settings/api` and provide it. Do not attempt to list files or proceed with any part of the workflow until the key is confirmed. ### 3. Schema-Aware Bookkeeping The agent is designed to handle API response changes gracefully: - **Dynamic Schema Evolution:** If the Recite API adds new information (new JSON fields), the agent will automatically add corresponding columns to your `bookkeeping_transactions.CSV` without losing existing data. - **Data Integrity Protection:** If a field that was previously present in the CSV is missing from the current API response, the agent will skip saving that specific entry and warn the user, preventing data corruption or "shifted" columns. ## Workflow & Features 1. **Scan Folder:** The agent scans the specified folder for images (`.jpg`, `.jpeg`, `.png`) and `.pdf` files. 2. **AI Extraction:** Calls the Recite API to extract `date`, `vendor`, `total`, `currency`, and `category`. 3. **Smart Renaming:** Renames the file to `[date]_[vendor].[ext]` (e.g., `2024-05-20_Starbucks.jpg`). 4. **Bookkeeping Log:** Appends the extracted data (Date, Vendor, Total, Currency, Category, Subtotal, Tax, Tip, Fees, Discounts, Description, Payment Method, Confidence, etc.) and filenames to `bookkeeping_transactions.CSV` in the target folder. 5. **Status Report:** Provides a summary of processed files and the CSV location. ## Long-Term Memory & Custom Logic Modify `skills/recite/long_term_memory.md` to add persistent instructions for the agent. The agent will always read this file before processing. Examples: - "After processing, move all files to a sub-folder named `processed/`." - "Alert me if any single receipt is over $500." - "Always categorize 'Amazon' as 'Software Services'." ## Tool Usage - **Command:** `python3 skills/recite/process_receipts.py <target_directory> skills/recite/` - **Arguments:** - `<target_directory>`: The folder containing your receipts. - `skills/recite/`: The path to the skill folder (used to locate `long_term_memory.md`). ## Strategic Moat - **Agent-First Consistency:** Guaranteed structured JSON output for financial data. - **Tax-Ready Logic:** Intelligent categorization based on standard business practices. - **Seamless Integration:** Built for the AI Agent economy (OpenClaw, Claude Code).

path: README.md
content: # Recite Agent Skill 🦞🤵

[![OpenClaw Skill](https://img.shields.io/badge/OpenClaw-Skill-blue.svg)](https://openclaw.ai)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**AI-powered receipt scanning & automated bookkeeping agent skill for OpenClaw and Claude Code.**

Recite allows your AI agent to handle the tedious work of receipt management. It scans images and PDFs using the Recite Vision API, renames files logically, and maintains a comprehensive local CSV ledger with 19+ financial data points.

---

## 🚀 Key Features (TL;DR for Agents)
- **Vision AI Extraction:** High-accuracy extraction of Date, Vendor, Total, Tax, Tip, Category, etc.
- **Smart Renaming:** Automatically renames files to `[YYYY-MM-DD]_[Vendor].png/pdf`.
- **Schema-Aware Bookkeeping:** Appends data to `bookkeeping_transactions.CSV`. Handles API response changes without data loss.
- **Local-First:** Keeps your financial data on your machine.
- **Long-Term Memory:** Supports custom persistent instructions (e.g., "Alert if total > $500").

## 🛠️ Setup

### 1. Get API Key
Obtain your Recite API key from: [https://recite.rivra.dev/settings/api](https://recite.rivra.dev/settings/api)

### 2. Configure
Set your API key in your environment or a config file:
- **Env:** `export RECITE_API_KEY="re_live_..."`
- **Config:** `~/.config/recite/config.json` -> `{"api_key": "re_live_..."}`

### 3. Dependencies
```bash
pip install requests
```

## 🤖 For AI Agents: How to use this Skill

If you are an AI agent, follow these steps to process receipts for your user:

1. **Verify Key:** Ensure `RECITE_API_KEY` is available.
2. **Read LTM:** Always read `skills/recite/long_term_memory.md` for user-specific rules.
3. **Execute:** Run the processing script:
   ```bash
   python3 skills/recite/process_receipts.py <path_to_receipts_folder> skills/recite/
   ```

## 📊 Data Points Captured
The skill maintains a local ledger with the following columns:
`Date`, `Vendor`, `Total`, `Currency`, `Category`, `Subtotal`, `Tax`, `Tip`, `Fees`, `Discounts`, `Description`, `PaymentMethod`, `Confidence`, `ScanID`, `OriginalFilename`, `NewFilename`.

## 📜 License
MIT License. Created by [Gavin J. (@RivraDev)](https://x.com/RivraDev).


path: long_term_memory.md
content: # Recite Long-Term Memory (Persistent Instructions)

This file contains persistent instructions for the Recite agent. Modify this file to customize your bookkeeping workflow.

## Custom Workflow Instructions
- **File Organization:** After scanning, you can instruct the agent to move files to specific sub-folders (e.g., `processed/2026/`).
- **Alerts & Thresholds:** Set thresholds for specific vendors or amounts (e.g., "Alert me if any single receipt is over $500").
- **Categorization Rules:** Define custom logic for specific vendors (e.g., "Always categorize 'Amazon' as 'Equipment'").

## User Preferences (Current)
- (No custom preferences set. Add yours here!)


path: process_receipts.py
content: import os
import json
import base64
import requests
import csv
import sys
import glob
from datetime import datetime

# Configuration
BASE_URL = "https://recite.rivra.dev/apiV1/api/v1"
CONFIG_PATH = os.path.expanduser("~/.config/recite/config.json")
CSV_NAME = "bookkeeping_transactions.CSV"
LTM_FILE = "long_term_memory.md"

def load_config():
    if not os.path.exists(CONFIG_PATH):
        return None
    with open(CONFIG_PATH, 'r') as f:
        return json.load(f)

def get_api_key():
    config = load_config()
    if config and "api_key" in config:
        return config["api_key"]
    return os.environ.get("RECITE_API_KEY")

def read_ltm(skill_path):
    ltm_path = os.path.join(skill_path, LTM_FILE)
    if os.path.exists(ltm_path):
        with open(ltm_path, 'r') as f:
            return f.read()
    return ""

def flatten_dict(d, parent_key='', sep='_'):
    """Helper to flatten nested dictionaries for CSV columns."""
    items = []
    for k, v in d.items():
        new_key = f"{parent_key}{sep}{k}" if parent_key else k
        if isinstance(v, dict):
            items.extend(flatten_dict(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

def scan_receipt(api_key, file_path):
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    
    with open(file_path, "rb") as image_file:
        encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
    
    mime_type = "image/jpeg"
    if file_path.lower().endswith('.pdf'):
        mime_type = "application/pdf"
    elif file_path.lower().endswith('.png'):
        mime_type = "image/png"
        
    payload = {
        "image_base64": f"data:{mime_type};base64,{encoded_string}"
    }
    
    try:
        response = requests.post(f"{BASE_URL}/scan", headers=headers, json=payload, timeout=30)
        if response.status_code == 200:
            return response.json()
        else:
            print(f"Error scanning {file_path}: {response.status_code} - {response.text}")
            return None
    except Exception as e:
        print(f"Request failed for {file_path}: {e}")
        return None

def update_csv_schema_and_append(csv_path, new_row_dict):
    """
    Handles CSV schema evolution:
    1. If a field is missing from API but exists in CSV, it leaves it blank.
    2. If a new field exists in API but not in CSV, it adds a new column.
    """
    file_exists = os.path.exists(csv_path)
    existing_data = []
    existing_headers = []

    if file_exists:
        with open(csv_path, 'r', newline='', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            existing_headers = reader.fieldnames if reader.fieldnames else []
            existing_data = list(reader)

    # Detect new fields not in existing headers
    new_fields = [k for k in new_row_dict.keys() if k not in existing_headers]
    
    if new_fields:
        print(f"New fields detected: {new_fields}. Expanding CSV columns...")
        existing_headers = existing_headers + new_fields
        # Always rewrite to ensure headers are updated and existing rows are aligned
        with open(csv_path, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=existing_headers, restval='')
            writer.writeheader()
            writer.writerows(existing_data)
            writer.writerow(new_row_dict)
    else:
        # Standard append if no new fields
        # DictWriter naturally handles missing keys in new_row_dict by using restval=''
        with open(csv_path, 'a', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=existing_headers, restval='')
            if not file_exists:
                writer.writeheader()
            writer.writerow(new_row_dict)
    
    return True

def main():
    if len(sys.argv) < 2:
        print("Usage: python3 process_receipts.py <directory_path> [skill_path]")
        sys.exit(1)
        
    target_dir = sys.argv[1]
    skill_path = sys.argv[2] if len(sys.argv) > 2 else "."
    
    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"Error: API Key not found.")
        sys.exit(1)
        
    # Supported extensions
    extensions = ['*.jpg', '*.jpeg', '*.png', '*.pdf']
    files = []
    for ext in extensions:
        files.extend(glob.glob(os.path.join(target_dir, ext)))
        
    if not files:
        print(f"No receipt files found in {target_dir}")
        return

    processed_count = 0
    csv_path = os.path.join(target_dir, CSV_NAME)
    
    for file_path in files:
        if CSV_NAME in file_path: continue
        
        print(f"Processing: {os.path.basename(file_path)}...")
        result = scan_receipt(api_key, file_path)
        
        if result and result.get('success'):
            # Flatten API response for CSV
            raw_data = result.get('data', {})
            row_data = flatten_dict(raw_data.get('extracted_data', {}))
            row_data['scan_id'] = raw_data.get('scan_id')
            row_data['transaction_type'] = raw_data.get('transaction_type')
            
            # File metadata
            ext = os.path.splitext(file_path)[1]
            date = row_data.get('date', 'UnknownDate')
            vendor = str(row_data.get('vendor', 'UnknownVendor')).replace('/', '-').replace('\\', '-')
            new_name = f"{date}_{vendor}{ext}"
            new_path = os.path.join(target_dir, new_name)
            
            # Name collision check
            if os.path.exists(new_path) and os.path.abspath(new_path) != os.path.abspath(file_path):
                new_name = f"{date}_{vendor}_{int(datetime.now().timestamp())}{ext}"
                new_path = os.path.join(target_dir, new_name)
            
            # Prepare row for CSV
            row_to_save = row_data.copy()
            row_to_save['OriginalFilename'] = os.path.basename(file_path)
            row_to_save['NewFilename'] = new_name
            
            if update_csv_schema_and_append(csv_path, row_to_save):
                try:
                    os.rename(file_path, new_path)
                    print(f"Successfully processed and renamed to: {new_name}")
                    processed_count += 1
                except Exception as e:
                    print(f"Error renaming: {e}")
            else:
                print(f"Failed to save record for {file_path}.")
        else:
            print(f"Skipping {file_path} due to API error.")

    print(f"\nProcessing Complete: {processed_count} files processed.")
    print(f"Transaction data saved to: {csv_path}")

if __name__ == "__main__":
    main()


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