文件预览

SKILL.md

查看 Plutus Pro — Full Expense Intelligence 技能包中的文件内容。

文件内容

SKILL.md

---
name: plutus-pro
description: "Plutus Pro — Wealth Intelligence. Full expense tracking with AI-powered categorisation, multi-account reconciliation, tax category tagging, budget forecasting, monthly P&L, and automated savings rate analysis. Works with CSV exports from any bank or app."
version: "1.0.4"
metadata:
  openclaw:
    requires:
      env: [LICENSE_KEY]
      bins: [python3, pip3]
    primaryEnv: "EXPENSES_FILE"
    homepage: https://clawhub.ai/occupythemilkyway/plutus-pro
    emoji: "💰📊⚡"
    tags: [finance, expenses, budget, bookkeeping, tracker, plutus, pro, premium, tax, forecasting]
    envVars:
      - name: LICENSE_KEY
        required: true
        description: "Your Plutus Pro license key. Get one at: ko-fi.com/s/83c662001e"

💰 **Bundle deal:** all 5 Pro skills for **$29** → **ko-fi.com/s/7625accf3f** (save $16)
      - name: EXPENSES_FILE
        required: false
        description: "Path to a CSV of transactions (date, description, amount)"
        default: ""
      - name: EXPENSES_TEXT
        required: false
        description: "Raw expense text, one per line"
        default: ""
      - name: BUDGET_JSON
        required: false
        description: "Monthly budget per category as JSON string"
        default: ""
      - name: SAVINGS_GOAL
        required: false
        description: "Monthly savings target amount for savings rate tracking"
        default: "0"
      - name: TAX_CATEGORIES
        required: false
        description: "Comma-separated categories to flag as tax-deductible (e.g. 'Business,Education')"
        default: "Business,Education"
      - name: CURRENCY
        required: false
        description: "Currency symbol: USD, EUR, GBP, CAD"
        default: "USD"
      - name: REPORT_MONTH
        required: false
        description: "Filter to YYYY-MM, leave blank for all"
        default: ""
      - name: FORECAST_MONTHS
        required: false
        description: "Months to project spending forward (1-12)"
        default: "3"
---

# Plutus Pro — Full Wealth Intelligence

Everything in Plutus, plus tax tagging, savings rate analysis, multi-month forecasting, P&L summary, and per-transaction notes.

## Pro features vs free Plutus

| Feature | Plutus (Free) | Plutus Pro |
|---------|--------------|-----------|
| Transactions | Unlimited | Unlimited |
| Categories | 15 standard | 15 + custom tax flags |
| Budget comparison | ✅ | ✅ + percentage alerts |
| Monthly trends | ✅ | ✅ + P&L summary |
| Tax category tagging | ❌ | ✅ |
| Savings rate analysis | ❌ | ✅ |
| Spending forecast | ❌ | ✅ 1-12 months |
| JSON export | ❌ | ✅ Full structured data |
| Surplus / deficit | ❌ | ✅ Monthly P&L |

---

## Setup

1. **Purchase** your license key at **ko-fi.com/s/83c662001e** ($9 one-time)
   - Or get all 5 Pro skills for **$29** → **ko-fi.com/s/7625accf3f** (save $16)
2. **Install:** `openclaw skills install plutus-pro`
3. **Activate:** set the `LICENSE_KEY` environment variable to the key you received
4. **Run** — you're in

---

## Step 1 — Install

```bash
pip3 install rich --break-system-packages --quiet
```

---

## Step 2 — Full wealth analysis (Pro)

```python
import os, re, json, csv
from datetime import datetime, date
from collections import defaultdict
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich import box

console = Console()

LICENSE_KEY = os.environ.get("LICENSE_KEY","").strip()
if not LICENSE_KEY:
    console.print(Panel(
        "[red bold]🔒 Plutus Pro requires a license key.[/red bold]\n\n"
        "Get your key at: [bold cyan]ko-fi.com/s/83c662001e[/bold cyan]\n\n"
        "Or use the free version: [dim]openclaw skills install plutus[/dim]",
        title="License Required", border_style="red"
    ))
    raise SystemExit(1)

EXPENSES_FILE  = os.environ.get("EXPENSES_FILE","").strip()
EXPENSES_TEXT  = os.environ.get("EXPENSES_TEXT","").strip()
BUDGET_RAW     = os.environ.get("BUDGET_JSON","").strip()
CURRENCY       = os.environ.get("CURRENCY","USD").upper()
REPORT_MONTH   = os.environ.get("REPORT_MONTH","").strip()
TAX_CATS_RAW   = os.environ.get("TAX_CATEGORIES","Business,Education")
TAX_CATEGORIES = [t.strip() for t in TAX_CATS_RAW.split(",") if t.strip()]
try: SAVINGS_GOAL   = float(os.environ.get("SAVINGS_GOAL","0"))
except: SAVINGS_GOAL = 0.0
try: FORECAST_MONTHS = min(int(os.environ.get("FORECAST_MONTHS","3")),12)
except: FORECAST_MONTHS = 3
TODAY = date.today()
SYM   = {"USD":"$","EUR":"€","GBP":"£","CAD":"CA$"}.get(CURRENCY,"$")

def fmt(a): return f"{SYM}{abs(a):,.2f}"

CATEGORIES = {
    "Food & Dining":    ["coffee","starbucks","restaurant","pizza","burger","cafe","dining","food","doordash","grubhub","grocery","groceries","walmart","whole foods","supermarket"],
    "Transport":        ["uber","lyft","taxi","gas","fuel","parking","transit","metro","bus","train","airline","flight","car rental","toll","petrol"],
    "Shopping":         ["amazon","ebay","etsy","target","bestbuy","clothing","shoes","fashion","zara","nordstrom","mall"],
    "Subscriptions":    ["netflix","spotify","hulu","disney","apple music","youtube","prime","subscription","membership","software","adobe","microsoft","google"],
    "Utilities":        ["electric","electricity","water","internet","phone","mobile","cellular","at&t","verizon","comcast","hydro","utility"],
    "Health":           ["pharmacy","doctor","dentist","medical","hospital","prescription","medicine","gym","fitness","yoga","cvs","walgreens"],
    "Entertainment":    ["movie","cinema","theatre","concert","ticket","game","gaming","steam","kindle","audible","museum"],
    "Travel":           ["hotel","airbnb","hostel","resort","booking","expedia","trip","vacation","tour"],
    "Education":        ["course","udemy","coursera","tuition","textbook","training","workshop","class","lesson"],
    "Home":             ["rent","mortgage","furniture","home depot","lowes","hardware","repair","maintenance","cleaning"],
    "Insurance":        ["insurance","premium","policy","geico","allstate","progressive"],
    "Business":         ["invoice","client","freelance","office","supplies","coworking","advertising","domain","hosting"],
    "Personal Care":    ["salon","haircut","barber","spa","beauty","cosmetics","skincare","makeup","nails"],
    "Income / Credit":  [],
}

def categorise(desc, amount):
    if amount < 0: return "Income / Credit"
    dl = desc.lower()
    for cat, kws in CATEGORIES.items():
        if cat == "Income / Credit": continue
        if any(k in dl for k in kws): return cat
    return "Other"

def parse_amount(raw):
    raw = str(raw).strip().lstrip("$£€").replace(",","")
    try: return float(raw)
    except: return None

MONTH_MAP = {"jan":1,"feb":2,"mar":3,"apr":4,"may":5,"jun":6,"jul":7,"aug":8,"sep":9,"oct":10,"nov":11,"dec":12}

def parse_date(raw):
    raw = str(raw).strip()
    for fmt_s in ("%Y-%m-%d","%m/%d/%Y","%d/%m/%Y","%m-%d-%Y"):
        try: return datetime.strptime(raw,fmt_s).date()
        except: pass
    import re as _re
    m = _re.match(r"([A-Za-z]+)\s+(\d{1,2})(?:\s+(\d{4}))?",raw)
    if m:
        mon = MONTH_MAP.get(m.group(1)[:3].lower())
        if mon:
            try: return date(int(m.group(3) or TODAY.year), mon, int(m.group(2)))
            except: pass
    return None

transactions = []

if EXPENSES_FILE and os.path.exists(EXPENSES_FILE):
    with open(EXPENSES_FILE,newline="",encoding="utf-8") as fh:
        reader = csv.DictReader(fh)
        hdrs = [h.lower().strip() for h in (reader.fieldnames or [])]
        amt_col  = next((h for h in hdrs if "amount" in h or "amt" in h or "cost" in h),None)
        date_col = next((h for h in hdrs if "date" in h or "day" in h),None)
        desc_col = next((h for h in hdrs if "desc" in h or "name" in h or "memo" in h or "narration" in h or "payee" in h),None)
        note_col = next((h for h in hdrs if "note" in h or "comment" in h or "tag" in h),None)
        if not amt_col:
            console.print(f"[red]❌ CSV needs an 'amount' column. Found: {hdrs}[/red]")
            raise SystemExit(1)
        for row in reader:
            rk = {k.lower().strip():v for k,v in row.items()}
            amt = parse_amount(rk.get(amt_col,"0"))
            if amt is None: continue
            transactions.append({
                "date": parse_date(rk.get(date_col,"")) or TODAY,
                "description": (rk.get(desc_col,"Unknown") or "Unknown").strip(),
                "amount": amt,
                "note": rk.get(note_col,"") if note_col else "",
            })
elif EXPENSES_TEXT:
    for line in EXPENSES_TEXT.strip().splitlines():
        line = line.strip()
        if not line: continue
        tokens = line.split()
        amt = None
        for tok in reversed(tokens):
            amt = parse_amount(tok)
            if amt is not None: break
        if amt is None: continue
        txn_date = None
        desc_start = 0
        if len(tokens)>=2:
            dt = parse_date(tokens[0]+" "+tokens[1])
            if dt: txn_date=dt; desc_start=2
            else:
                dt = parse_date(tokens[0])
                if dt: txn_date=dt; desc_start=1
        desc_tokens = [t for t in tokens[desc_start:] if parse_amount(t)!=amt]
        transactions.append({"date":txn_date or TODAY,"description":" ".join(desc_tokens) or "Unknown","amount":amt,"note":""})
else:
    console.print("[yellow]ℹ️  No data set — running with demo data.[/yellow]\n")
    demo = [
        ("2025-01-05","Starbucks coffee",5.50,""),("2025-01-08","Uber ride",18.30,""),("2025-01-10","Netflix",15.99,""),
        ("2025-01-12","Groceries Walmart",87.45,""),("2025-01-14","Amazon order",34.99,""),
        ("2025-01-18","Restaurant dinner",62.00,""),("2025-01-20","Gas station",55.00,""),
        ("2025-01-22","Spotify",9.99,""),("2025-01-25","CVS pharmacy",22.10,""),("2025-01-28","Gym membership",45.00,""),
        ("2025-01-30","Udemy course",19.99,"tax"),("2025-01-31","Client payment",-500.00,"income"),
        ("2025-02-02","Coffee",4.80,""),("2025-02-05","Electric bill",110.00,""),
        ("2025-02-08","Uber eats",28.50,""),("2025-02-12","Whole Foods",93.20,""),
        ("2025-02-15","Freelance income",-800.00,"income"),("2025-02-18","Office supplies",45.00,"tax"),
        ("2025-02-20","Doctor visit",30.00,""),("2025-02-25","Movie tickets",28.00,""),
        ("2025-02-28","Domain hosting",12.00,"tax"),
    ]
    for d,desc,amt,note in demo:
        transactions.append({"date":parse_date(d) or TODAY,"description":desc,"amount":amt,"note":note})

if REPORT_MONTH:
    try:
        fd = datetime.strptime(REPORT_MONTH,"%Y-%m")
        transactions = [t for t in transactions if t["date"].year==fd.year and t["date"].month==fd.month]
    except ValueError:
        console.print("[red]❌ REPORT_MONTH must be YYYY-MM[/red]"); raise SystemExit(1)

for t in transactions:
    t["category"] = categorise(t["description"],t["amount"])
    t["tax_deductible"] = t["category"] in TAX_CATEGORIES and t["amount"] > 0

budget = {}
if BUDGET_RAW:
    try: budget = {k.title():float(v) for k,v in json.loads(BUDGET_RAW).items()}
    except: console.print("[yellow]⚠️  BUDGET_JSON invalid — skipping budget comparison.[/yellow]")

# Aggregates
cat_totals = defaultdict(float)
for t in transactions: cat_totals[t["category"]] += t["amount"]

expenses_only = {k:v for k,v in cat_totals.items() if v > 0}
credits       = abs(cat_totals.get("Income / Credit",0))
total_spend   = sum(expenses_only.values())
net           = credits - total_spend
savings_rate  = (net / credits * 100) if credits > 0 else 0

tax_total = sum(t["amount"] for t in transactions if t.get("tax_deductible"))

# Monthly aggregates
monthly = defaultdict(lambda: defaultdict(float))
monthly_income = defaultdict(float)
for t in transactions:
    mo = t["date"].strftime("%Y-%m")
    if t["amount"] > 0: monthly[mo][t["category"]] += t["amount"]
    else: monthly_income[mo] += abs(t["amount"])
months_sorted = sorted(set(list(monthly.keys())+list(monthly_income.keys())))

# Header
console.print()
console.print(Panel.fit(
    f"[bold green]💰📊⚡ Plutus Pro — Wealth Intelligence[/bold green]\n"
    f"Transactions: [yellow]{len(transactions)}[/yellow]  "
    f"Spend: [red]{fmt(total_spend)}[/red]  "
    f"Income: [green]{fmt(credits)}[/green]  "
    f"Net: [{'green' if net>=0 else 'red'}]{('+' if net>=0 else '')}{fmt(net)}[/{'green' if net>=0 else 'red'}]  "
    f"Tax-deductible: [cyan]{fmt(tax_total)}[/cyan]",
    border_style="green"
))

# Savings rate
if credits > 0:
    console.print()
    bar_filled = max(0,min(20,int(savings_rate/5)))
    bar = "█"*bar_filled+"░"*(20-bar_filled)
    goal_line = f"  Goal: {SAVINGS_GOAL:.0f}%" if SAVINGS_GOAL else ""
    console.print(Panel(
        f"[cyan]{bar}[/cyan]  [yellow]{savings_rate:.1f}% savings rate[/yellow]{goal_line}\n"
        f"Income: {fmt(credits)}  Spend: {fmt(total_spend)}  Net: {('+' if net>=0 else '')}{fmt(net)}",
        title="💰 Monthly P&L", border_style="green"
    ))

# Category totals
console.print()
tbl = Table(title="Spend by Category", box=box.ROUNDED, border_style="green")
tbl.add_column("Category",   width=20, style="cyan")
tbl.add_column(f"Total",     width=13, justify="right", style="red")
tbl.add_column("% Spend",    width=10, justify="right", style="yellow")
tbl.add_column("Budget",     width=12, justify="right", style="dim")
tbl.add_column("Status",     width=14)
tbl.add_column("Tax",        width=5)
for cat,total in sorted(expenses_only.items(),key=lambda x:-x[1]):
    pct  = total/total_spend*100 if total_spend else 0
    bgt  = budget.get(cat)
    over = total - bgt if bgt else 0
    status = f"[green]✅ OK[/green]" if bgt and total<=bgt else (f"[red]⚠ +{fmt(over)}[/red]" if bgt else "")
    bgt_s  = fmt(bgt) if bgt else "—"
    tax_s  = "✓" if cat in TAX_CATEGORIES else ""
    tbl.add_row(cat,fmt(total),f"{pct:.1f}%",bgt_s,status,f"[cyan]{tax_s}[/cyan]")
if credits:
    tbl.add_row("[green]Income / Credits[/green]",f"[green]-{fmt(credits)}[/green]","","","","")
console.print(tbl)

# Tax summary
if tax_total:
    console.print()
    tax_items = [t for t in transactions if t.get("tax_deductible")]
    console.print(Panel(
        f"[cyan]Total potential deductions: {fmt(tax_total)}[/cyan]\n\n" +
        "\n".join(f"• {t['date'].strftime('%b %d')} — {t['description']}: {fmt(t['amount'])}" for t in tax_items),
        title="🧾 Tax-Deductible Expenses",
        border_style="cyan"
    ))

# Monthly trend
if len(months_sorted)>1:
    console.print()
    trend = Table(title="Monthly Trends",box=box.SIMPLE,border_style="blue")
    trend.add_column("Month",width=10,style="cyan")
    trend.add_column("Spend",width=12,justify="right",style="red")
    trend.add_column("Income",width=12,justify="right",style="green")
    trend.add_column("Net",width=12,justify="right")
    for mo in months_sorted:
        sp = sum(monthly[mo].values())
        inc = monthly_income.get(mo,0)
        net_mo = inc-sp
        net_col = "green" if net_mo>=0 else "red"
        trend.add_row(mo,fmt(sp),fmt(inc) if inc else "—",f"[{net_col}]{('+' if net_mo>=0 else '')}{fmt(net_mo)}[/{net_col}]")
    console.print(trend)

# Forecast
if FORECAST_MONTHS>0 and total_spend>0:
    months_count = max(len(months_sorted),1)
    avg_monthly  = total_spend/months_count
    console.print()
    fc_lines = "\n".join(
        f"[dim]+{i}mo:[/dim] [red]{fmt(avg_monthly*(i+1))}[/red] projected spend  "
        f"([green]-{fmt(credits/months_count*(i+1))}[/green] projected income)"
        for i in range(FORECAST_MONTHS)
    )
    console.print(Panel(fc_lines,title=f"📈 {FORECAST_MONTHS}-Month Forecast (based on {months_count}-month average)",border_style="magenta"))

# Top transactions
console.print()
top = sorted([t for t in transactions if t["amount"]>0],key=lambda x:-x["amount"])[:10]
top_tbl = Table(title="Top 10 Transactions",box=box.ROUNDED,border_style="yellow")
top_tbl.add_column("Date",width=12,style="dim")
top_tbl.add_column("Description",width=28)
top_tbl.add_column("Category",width=18,style="cyan")
top_tbl.add_column("Amount",width=12,justify="right",style="red")
top_tbl.add_column("Tax",width=4)
for t in top:
    top_tbl.add_row(t["date"].strftime("%b %d, %Y"),t["description"][:26],t["category"],fmt(t["amount"]),"✓" if t.get("tax_deductible") else "")
console.print(top_tbl)

# Save
slug     = REPORT_MONTH or TODAY.strftime("%Y-%m")
md_path  = f"plutus_pro_report_{slug}.md"
csv_path = f"plutus_pro_summary_{slug}.csv"
json_path= f"plutus_pro_data_{slug}.json"

with open(md_path,"w",encoding="utf-8") as f:
    f.write(f"# 💰 Plutus Pro Report — {slug}\n\n")
    f.write(f"**Spend:** {fmt(total_spend)}  **Income:** {fmt(credits)}  **Net:** {('+' if net>=0 else '')}{fmt(net)}  **Tax deductible:** {fmt(tax_total)}\n\n")
    f.write(f"**Savings rate:** {savings_rate:.1f}%\n\n")
    f.write("## By Category\n\n| Category | Amount | % | Tax |\n|---|---|---|---|\n")
    for cat,total in sorted(expenses_only.items(),key=lambda x:-x[1]):
        pct=total/total_spend*100 if total_spend else 0
        f.write(f"| {cat} | {fmt(total)} | {pct:.1f}% | {'✓' if cat in TAX_CATEGORIES else ''} |\n")
    f.write("\n## All Transactions\n\n| Date | Description | Category | Amount | Tax |\n|---|---|---|---|---|\n")
    for t in sorted(transactions,key=lambda x:x["date"]):
        sign="-" if t["amount"]<0 else ""
        f.write(f"| {t['date'].strftime('%b %d')} | {t['description']} | {t['category']} | {sign}{fmt(t['amount'])} | {'✓' if t.get('tax_deductible') else ''} |\n")

with open(csv_path,"w",newline="",encoding="utf-8") as f:
    writer=csv.writer(f)
    writer.writerow(["category","total","pct","budget","over_budget","tax_deductible"])
    for cat,total in sorted(expenses_only.items(),key=lambda x:-x[1]):
        pct=total/total_spend*100 if total_spend else 0
        bgt=budget.get(cat,0)
        writer.writerow([cat,f"{total:.2f}",f"{pct:.1f}",f"{bgt:.2f}",f"{max(0,total-bgt):.2f}","yes" if cat in TAX_CATEGORIES else "no"])

with open(json_path,"w",encoding="utf-8") as f:
    json.dump({"period":slug,"summary":{"total_spend":total_spend,"total_income":credits,"net":net,"savings_rate":savings_rate,"tax_deductible":tax_total},"categories":{k:v for k,v in expenses_only.items()},"transactions":[{"date":str(t["date"]),"description":t["description"],"amount":t["amount"],"category":t["category"],"tax":t.get("tax_deductible",False)} for t in transactions]},f,indent=2)

console.print()
console.print(Panel(
    f"[green]✅ Done![/green]\n\n"
    f"📝 [cyan]{md_path}[/cyan]\n"
    f"📊 [cyan]{csv_path}[/cyan]\n"
    f"📄 [cyan]{json_path}[/cyan]",
    title="Exports", border_style="green"
))
```