{"skill":{"slug":"plutus-pro","displayName":"Plutus Pro — Full Expense Intelligence","summary":"Plutus Pro â Wealth Intelligence. Full expense tracking with AI-powered categorisation, multi-account reconciliation, tax category tagging, budget forecast...","description":"---\nname: plutus-pro\ndescription: \"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.\"\nversion: \"1.0.4\"\nmetadata:\n  openclaw:\n    requires:\n      env: [LICENSE_KEY]\n      bins: [python3, pip3]\n    primaryEnv: \"EXPENSES_FILE\"\n    homepage: https://clawhub.ai/occupythemilkyway/plutus-pro\n    emoji: \"💰📊⚡\"\n    tags: [finance, expenses, budget, bookkeeping, tracker, plutus, pro, premium, tax, forecasting]\n    envVars:\n      - name: LICENSE_KEY\n        required: true\n        description: \"Your Plutus Pro license key. Get one at: ko-fi.com/s/83c662001e\"\n\n💰 **Bundle deal:** all 5 Pro skills for **$29** → **ko-fi.com/s/7625accf3f** (save $16)\n      - name: EXPENSES_FILE\n        required: false\n        description: \"Path to a CSV of transactions (date, description, amount)\"\n        default: \"\"\n      - name: EXPENSES_TEXT\n        required: false\n        description: \"Raw expense text, one per line\"\n        default: \"\"\n      - name: BUDGET_JSON\n        required: false\n        description: \"Monthly budget per category as JSON string\"\n        default: \"\"\n      - name: SAVINGS_GOAL\n        required: false\n        description: \"Monthly savings target amount for savings rate tracking\"\n        default: \"0\"\n      - name: TAX_CATEGORIES\n        required: false\n        description: \"Comma-separated categories to flag as tax-deductible (e.g. 'Business,Education')\"\n        default: \"Business,Education\"\n      - name: CURRENCY\n        required: false\n        description: \"Currency symbol: USD, EUR, GBP, CAD\"\n        default: \"USD\"\n      - name: REPORT_MONTH\n        required: false\n        description: \"Filter to YYYY-MM, leave blank for all\"\n        default: \"\"\n      - name: FORECAST_MONTHS\n        required: false\n        description: \"Months to project spending forward (1-12)\"\n        default: \"3\"\n---\n\n# Plutus Pro — Full Wealth Intelligence\n\nEverything in Plutus, plus tax tagging, savings rate analysis, multi-month forecasting, P&L summary, and per-transaction notes.\n\n## Pro features vs free Plutus\n\n| Feature | Plutus (Free) | Plutus Pro |\n|---------|--------------|-----------|\n| Transactions | Unlimited | Unlimited |\n| Categories | 15 standard | 15 + custom tax flags |\n| Budget comparison | ✅ | ✅ + percentage alerts |\n| Monthly trends | ✅ | ✅ + P&L summary |\n| Tax category tagging | ❌ | ✅ |\n| Savings rate analysis | ❌ | ✅ |\n| Spending forecast | ❌ | ✅ 1-12 months |\n| JSON export | ❌ | ✅ Full structured data |\n| Surplus / deficit | ❌ | ✅ Monthly P&L |\n\n---\n\n## Setup\n\n1. **Purchase** your license key at **ko-fi.com/s/83c662001e** ($9 one-time)\n   - Or get all 5 Pro skills for **$29** → **ko-fi.com/s/7625accf3f** (save $16)\n2. **Install:** `openclaw skills install plutus-pro`\n3. **Activate:** set the `LICENSE_KEY` environment variable to the key you received\n4. **Run** — you're in\n\n---\n\n## Step 1 — Install\n\n```bash\npip3 install rich --break-system-packages --quiet\n```\n\n---\n\n## Step 2 — Full wealth analysis (Pro)\n\n```python\nimport os, re, json, csv\nfrom datetime import datetime, date\nfrom collections import defaultdict\nfrom rich.console import Console\nfrom rich.table import Table\nfrom rich.panel import Panel\nfrom rich import box\n\nconsole = Console()\n\nLICENSE_KEY = os.environ.get(\"LICENSE_KEY\",\"\").strip()\nif not LICENSE_KEY:\n    console.print(Panel(\n        \"[red bold]🔒 Plutus Pro requires a license key.[/red bold]\\n\\n\"\n        \"Get your key at: [bold cyan]ko-fi.com/s/83c662001e[/bold cyan]\\n\\n\"\n        \"Or use the free version: [dim]openclaw skills install plutus[/dim]\",\n        title=\"License Required\", border_style=\"red\"\n    ))\n    raise SystemExit(1)\n\nEXPENSES_FILE  = os.environ.get(\"EXPENSES_FILE\",\"\").strip()\nEXPENSES_TEXT  = os.environ.get(\"EXPENSES_TEXT\",\"\").strip()\nBUDGET_RAW     = os.environ.get(\"BUDGET_JSON\",\"\").strip()\nCURRENCY       = os.environ.get(\"CURRENCY\",\"USD\").upper()\nREPORT_MONTH   = os.environ.get(\"REPORT_MONTH\",\"\").strip()\nTAX_CATS_RAW   = os.environ.get(\"TAX_CATEGORIES\",\"Business,Education\")\nTAX_CATEGORIES = [t.strip() for t in TAX_CATS_RAW.split(\",\") if t.strip()]\ntry: SAVINGS_GOAL   = float(os.environ.get(\"SAVINGS_GOAL\",\"0\"))\nexcept: SAVINGS_GOAL = 0.0\ntry: FORECAST_MONTHS = min(int(os.environ.get(\"FORECAST_MONTHS\",\"3\")),12)\nexcept: FORECAST_MONTHS = 3\nTODAY = date.today()\nSYM   = {\"USD\":\"$\",\"EUR\":\"€\",\"GBP\":\"£\",\"CAD\":\"CA$\"}.get(CURRENCY,\"$\")\n\ndef fmt(a): return f\"{SYM}{abs(a):,.2f}\"\n\nCATEGORIES = {\n    \"Food & Dining\":    [\"coffee\",\"starbucks\",\"restaurant\",\"pizza\",\"burger\",\"cafe\",\"dining\",\"food\",\"doordash\",\"grubhub\",\"grocery\",\"groceries\",\"walmart\",\"whole foods\",\"supermarket\"],\n    \"Transport\":        [\"uber\",\"lyft\",\"taxi\",\"gas\",\"fuel\",\"parking\",\"transit\",\"metro\",\"bus\",\"train\",\"airline\",\"flight\",\"car rental\",\"toll\",\"petrol\"],\n    \"Shopping\":         [\"amazon\",\"ebay\",\"etsy\",\"target\",\"bestbuy\",\"clothing\",\"shoes\",\"fashion\",\"zara\",\"nordstrom\",\"mall\"],\n    \"Subscriptions\":    [\"netflix\",\"spotify\",\"hulu\",\"disney\",\"apple music\",\"youtube\",\"prime\",\"subscription\",\"membership\",\"software\",\"adobe\",\"microsoft\",\"google\"],\n    \"Utilities\":        [\"electric\",\"electricity\",\"water\",\"internet\",\"phone\",\"mobile\",\"cellular\",\"at&t\",\"verizon\",\"comcast\",\"hydro\",\"utility\"],\n    \"Health\":           [\"pharmacy\",\"doctor\",\"dentist\",\"medical\",\"hospital\",\"prescription\",\"medicine\",\"gym\",\"fitness\",\"yoga\",\"cvs\",\"walgreens\"],\n    \"Entertainment\":    [\"movie\",\"cinema\",\"theatre\",\"concert\",\"ticket\",\"game\",\"gaming\",\"steam\",\"kindle\",\"audible\",\"museum\"],\n    \"Travel\":           [\"hotel\",\"airbnb\",\"hostel\",\"resort\",\"booking\",\"expedia\",\"trip\",\"vacation\",\"tour\"],\n    \"Education\":        [\"course\",\"udemy\",\"coursera\",\"tuition\",\"textbook\",\"training\",\"workshop\",\"class\",\"lesson\"],\n    \"Home\":             [\"rent\",\"mortgage\",\"furniture\",\"home depot\",\"lowes\",\"hardware\",\"repair\",\"maintenance\",\"cleaning\"],\n    \"Insurance\":        [\"insurance\",\"premium\",\"policy\",\"geico\",\"allstate\",\"progressive\"],\n    \"Business\":         [\"invoice\",\"client\",\"freelance\",\"office\",\"supplies\",\"coworking\",\"advertising\",\"domain\",\"hosting\"],\n    \"Personal Care\":    [\"salon\",\"haircut\",\"barber\",\"spa\",\"beauty\",\"cosmetics\",\"skincare\",\"makeup\",\"nails\"],\n    \"Income / Credit\":  [],\n}\n\ndef categorise(desc, amount):\n    if amount < 0: return \"Income / Credit\"\n    dl = desc.lower()\n    for cat, kws in CATEGORIES.items():\n        if cat == \"Income / Credit\": continue\n        if any(k in dl for k in kws): return cat\n    return \"Other\"\n\ndef parse_amount(raw):\n    raw = str(raw).strip().lstrip(\"$£€\").replace(\",\",\"\")\n    try: return float(raw)\n    except: return None\n\nMONTH_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}\n\ndef parse_date(raw):\n    raw = str(raw).strip()\n    for fmt_s in (\"%Y-%m-%d\",\"%m/%d/%Y\",\"%d/%m/%Y\",\"%m-%d-%Y\"):\n        try: return datetime.strptime(raw,fmt_s).date()\n        except: pass\n    import re as _re\n    m = _re.match(r\"([A-Za-z]+)\\s+(\\d{1,2})(?:\\s+(\\d{4}))?\",raw)\n    if m:\n        mon = MONTH_MAP.get(m.group(1)[:3].lower())\n        if mon:\n            try: return date(int(m.group(3) or TODAY.year), mon, int(m.group(2)))\n            except: pass\n    return None\n\ntransactions = []\n\nif EXPENSES_FILE and os.path.exists(EXPENSES_FILE):\n    with open(EXPENSES_FILE,newline=\"\",encoding=\"utf-8\") as fh:\n        reader = csv.DictReader(fh)\n        hdrs = [h.lower().strip() for h in (reader.fieldnames or [])]\n        amt_col  = next((h for h in hdrs if \"amount\" in h or \"amt\" in h or \"cost\" in h),None)\n        date_col = next((h for h in hdrs if \"date\" in h or \"day\" in h),None)\n        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)\n        note_col = next((h for h in hdrs if \"note\" in h or \"comment\" in h or \"tag\" in h),None)\n        if not amt_col:\n            console.print(f\"[red]❌ CSV needs an 'amount' column. Found: {hdrs}[/red]\")\n            raise SystemExit(1)\n        for row in reader:\n            rk = {k.lower().strip():v for k,v in row.items()}\n            amt = parse_amount(rk.get(amt_col,\"0\"))\n            if amt is None: continue\n            transactions.append({\n                \"date\": parse_date(rk.get(date_col,\"\")) or TODAY,\n                \"description\": (rk.get(desc_col,\"Unknown\") or \"Unknown\").strip(),\n                \"amount\": amt,\n                \"note\": rk.get(note_col,\"\") if note_col else \"\",\n            })\nelif EXPENSES_TEXT:\n    for line in EXPENSES_TEXT.strip().splitlines():\n        line = line.strip()\n        if not line: continue\n        tokens = line.split()\n        amt = None\n        for tok in reversed(tokens):\n            amt = parse_amount(tok)\n            if amt is not None: break\n        if amt is None: continue\n        txn_date = None\n        desc_start = 0\n        if len(tokens)>=2:\n            dt = parse_date(tokens[0]+\" \"+tokens[1])\n            if dt: txn_date=dt; desc_start=2\n            else:\n                dt = parse_date(tokens[0])\n                if dt: txn_date=dt; desc_start=1\n        desc_tokens = [t for t in tokens[desc_start:] if parse_amount(t)!=amt]\n        transactions.append({\"date\":txn_date or TODAY,\"description\":\" \".join(desc_tokens) or \"Unknown\",\"amount\":amt,\"note\":\"\"})\nelse:\n    console.print(\"[yellow]ℹ️  No data set — running with demo data.[/yellow]\\n\")\n    demo = [\n        (\"2025-01-05\",\"Starbucks coffee\",5.50,\"\"),(\"2025-01-08\",\"Uber ride\",18.30,\"\"),(\"2025-01-10\",\"Netflix\",15.99,\"\"),\n        (\"2025-01-12\",\"Groceries Walmart\",87.45,\"\"),(\"2025-01-14\",\"Amazon order\",34.99,\"\"),\n        (\"2025-01-18\",\"Restaurant dinner\",62.00,\"\"),(\"2025-01-20\",\"Gas station\",55.00,\"\"),\n        (\"2025-01-22\",\"Spotify\",9.99,\"\"),(\"2025-01-25\",\"CVS pharmacy\",22.10,\"\"),(\"2025-01-28\",\"Gym membership\",45.00,\"\"),\n        (\"2025-01-30\",\"Udemy course\",19.99,\"tax\"),(\"2025-01-31\",\"Client payment\",-500.00,\"income\"),\n        (\"2025-02-02\",\"Coffee\",4.80,\"\"),(\"2025-02-05\",\"Electric bill\",110.00,\"\"),\n        (\"2025-02-08\",\"Uber eats\",28.50,\"\"),(\"2025-02-12\",\"Whole Foods\",93.20,\"\"),\n        (\"2025-02-15\",\"Freelance income\",-800.00,\"income\"),(\"2025-02-18\",\"Office supplies\",45.00,\"tax\"),\n        (\"2025-02-20\",\"Doctor visit\",30.00,\"\"),(\"2025-02-25\",\"Movie tickets\",28.00,\"\"),\n        (\"2025-02-28\",\"Domain hosting\",12.00,\"tax\"),\n    ]\n    for d,desc,amt,note in demo:\n        transactions.append({\"date\":parse_date(d) or TODAY,\"description\":desc,\"amount\":amt,\"note\":note})\n\nif REPORT_MONTH:\n    try:\n        fd = datetime.strptime(REPORT_MONTH,\"%Y-%m\")\n        transactions = [t for t in transactions if t[\"date\"].year==fd.year and t[\"date\"].month==fd.month]\n    except ValueError:\n        console.print(\"[red]❌ REPORT_MONTH must be YYYY-MM[/red]\"); raise SystemExit(1)\n\nfor t in transactions:\n    t[\"category\"] = categorise(t[\"description\"],t[\"amount\"])\n    t[\"tax_deductible\"] = t[\"category\"] in TAX_CATEGORIES and t[\"amount\"] > 0\n\nbudget = {}\nif BUDGET_RAW:\n    try: budget = {k.title():float(v) for k,v in json.loads(BUDGET_RAW).items()}\n    except: console.print(\"[yellow]⚠️  BUDGET_JSON invalid — skipping budget comparison.[/yellow]\")\n\n# Aggregates\ncat_totals = defaultdict(float)\nfor t in transactions: cat_totals[t[\"category\"]] += t[\"amount\"]\n\nexpenses_only = {k:v for k,v in cat_totals.items() if v > 0}\ncredits       = abs(cat_totals.get(\"Income / Credit\",0))\ntotal_spend   = sum(expenses_only.values())\nnet           = credits - total_spend\nsavings_rate  = (net / credits * 100) if credits > 0 else 0\n\ntax_total = sum(t[\"amount\"] for t in transactions if t.get(\"tax_deductible\"))\n\n# Monthly aggregates\nmonthly = defaultdict(lambda: defaultdict(float))\nmonthly_income = defaultdict(float)\nfor t in transactions:\n    mo = t[\"date\"].strftime(\"%Y-%m\")\n    if t[\"amount\"] > 0: monthly[mo][t[\"category\"]] += t[\"amount\"]\n    else: monthly_income[mo] += abs(t[\"amount\"])\nmonths_sorted = sorted(set(list(monthly.keys())+list(monthly_income.keys())))\n\n# Header\nconsole.print()\nconsole.print(Panel.fit(\n    f\"[bold green]💰📊⚡ Plutus Pro — Wealth Intelligence[/bold green]\\n\"\n    f\"Transactions: [yellow]{len(transactions)}[/yellow]  \"\n    f\"Spend: [red]{fmt(total_spend)}[/red]  \"\n    f\"Income: [green]{fmt(credits)}[/green]  \"\n    f\"Net: [{'green' if net>=0 else 'red'}]{('+' if net>=0 else '')}{fmt(net)}[/{'green' if net>=0 else 'red'}]  \"\n    f\"Tax-deductible: [cyan]{fmt(tax_total)}[/cyan]\",\n    border_style=\"green\"\n))\n\n# Savings rate\nif credits > 0:\n    console.print()\n    bar_filled = max(0,min(20,int(savings_rate/5)))\n    bar = \"█\"*bar_filled+\"░\"*(20-bar_filled)\n    goal_line = f\"  Goal: {SAVINGS_GOAL:.0f}%\" if SAVINGS_GOAL else \"\"\n    console.print(Panel(\n        f\"[cyan]{bar}[/cyan]  [yellow]{savings_rate:.1f}% savings rate[/yellow]{goal_line}\\n\"\n        f\"Income: {fmt(credits)}  Spend: {fmt(total_spend)}  Net: {('+' if net>=0 else '')}{fmt(net)}\",\n        title=\"💰 Monthly P&L\", border_style=\"green\"\n    ))\n\n# Category totals\nconsole.print()\ntbl = Table(title=\"Spend by Category\", box=box.ROUNDED, border_style=\"green\")\ntbl.add_column(\"Category\",   width=20, style=\"cyan\")\ntbl.add_column(f\"Total\",     width=13, justify=\"right\", style=\"red\")\ntbl.add_column(\"% Spend\",    width=10, justify=\"right\", style=\"yellow\")\ntbl.add_column(\"Budget\",     width=12, justify=\"right\", style=\"dim\")\ntbl.add_column(\"Status\",     width=14)\ntbl.add_column(\"Tax\",        width=5)\nfor cat,total in sorted(expenses_only.items(),key=lambda x:-x[1]):\n    pct  = total/total_spend*100 if total_spend else 0\n    bgt  = budget.get(cat)\n    over = total - bgt if bgt else 0\n    status = f\"[green]✅ OK[/green]\" if bgt and total<=bgt else (f\"[red]⚠ +{fmt(over)}[/red]\" if bgt else \"\")\n    bgt_s  = fmt(bgt) if bgt else \"—\"\n    tax_s  = \"✓\" if cat in TAX_CATEGORIES else \"\"\n    tbl.add_row(cat,fmt(total),f\"{pct:.1f}%\",bgt_s,status,f\"[cyan]{tax_s}[/cyan]\")\nif credits:\n    tbl.add_row(\"[green]Income / Credits[/green]\",f\"[green]-{fmt(credits)}[/green]\",\"\",\"\",\"\",\"\")\nconsole.print(tbl)\n\n# Tax summary\nif tax_total:\n    console.print()\n    tax_items = [t for t in transactions if t.get(\"tax_deductible\")]\n    console.print(Panel(\n        f\"[cyan]Total potential deductions: {fmt(tax_total)}[/cyan]\\n\\n\" +\n        \"\\n\".join(f\"• {t['date'].strftime('%b %d')} — {t['description']}: {fmt(t['amount'])}\" for t in tax_items),\n        title=\"🧾 Tax-Deductible Expenses\",\n        border_style=\"cyan\"\n    ))\n\n# Monthly trend\nif len(months_sorted)>1:\n    console.print()\n    trend = Table(title=\"Monthly Trends\",box=box.SIMPLE,border_style=\"blue\")\n    trend.add_column(\"Month\",width=10,style=\"cyan\")\n    trend.add_column(\"Spend\",width=12,justify=\"right\",style=\"red\")\n    trend.add_column(\"Income\",width=12,justify=\"right\",style=\"green\")\n    trend.add_column(\"Net\",width=12,justify=\"right\")\n    for mo in months_sorted:\n        sp = sum(monthly[mo].values())\n        inc = monthly_income.get(mo,0)\n        net_mo = inc-sp\n        net_col = \"green\" if net_mo>=0 else \"red\"\n        trend.add_row(mo,fmt(sp),fmt(inc) if inc else \"—\",f\"[{net_col}]{('+' if net_mo>=0 else '')}{fmt(net_mo)}[/{net_col}]\")\n    console.print(trend)\n\n# Forecast\nif FORECAST_MONTHS>0 and total_spend>0:\n    months_count = max(len(months_sorted),1)\n    avg_monthly  = total_spend/months_count\n    console.print()\n    fc_lines = \"\\n\".join(\n        f\"[dim]+{i}mo:[/dim] [red]{fmt(avg_monthly*(i+1))}[/red] projected spend  \"\n        f\"([green]-{fmt(credits/months_count*(i+1))}[/green] projected income)\"\n        for i in range(FORECAST_MONTHS)\n    )\n    console.print(Panel(fc_lines,title=f\"📈 {FORECAST_MONTHS}-Month Forecast (based on {months_count}-month average)\",border_style=\"magenta\"))\n\n# Top transactions\nconsole.print()\ntop = sorted([t for t in transactions if t[\"amount\"]>0],key=lambda x:-x[\"amount\"])[:10]\ntop_tbl = Table(title=\"Top 10 Transactions\",box=box.ROUNDED,border_style=\"yellow\")\ntop_tbl.add_column(\"Date\",width=12,style=\"dim\")\ntop_tbl.add_column(\"Description\",width=28)\ntop_tbl.add_column(\"Category\",width=18,style=\"cyan\")\ntop_tbl.add_column(\"Amount\",width=12,justify=\"right\",style=\"red\")\ntop_tbl.add_column(\"Tax\",width=4)\nfor t in top:\n    top_tbl.add_row(t[\"date\"].strftime(\"%b %d, %Y\"),t[\"description\"][:26],t[\"category\"],fmt(t[\"amount\"]),\"✓\" if t.get(\"tax_deductible\") else \"\")\nconsole.print(top_tbl)\n\n# Save\nslug     = REPORT_MONTH or TODAY.strftime(\"%Y-%m\")\nmd_path  = f\"plutus_pro_report_{slug}.md\"\ncsv_path = f\"plutus_pro_summary_{slug}.csv\"\njson_path= f\"plutus_pro_data_{slug}.json\"\n\nwith open(md_path,\"w\",encoding=\"utf-8\") as f:\n    f.write(f\"# 💰 Plutus Pro Report — {slug}\\n\\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\")\n    f.write(f\"**Savings rate:** {savings_rate:.1f}%\\n\\n\")\n    f.write(\"## By Category\\n\\n| Category | Amount | % | Tax |\\n|---|---|---|---|\\n\")\n    for cat,total in sorted(expenses_only.items(),key=lambda x:-x[1]):\n        pct=total/total_spend*100 if total_spend else 0\n        f.write(f\"| {cat} | {fmt(total)} | {pct:.1f}% | {'✓' if cat in TAX_CATEGORIES else ''} |\\n\")\n    f.write(\"\\n## All Transactions\\n\\n| Date | Description | Category | Amount | Tax |\\n|---|---|---|---|---|\\n\")\n    for t in sorted(transactions,key=lambda x:x[\"date\"]):\n        sign=\"-\" if t[\"amount\"]<0 else \"\"\n        f.write(f\"| {t['date'].strftime('%b %d')} | {t['description']} | {t['category']} | {sign}{fmt(t['amount'])} | {'✓' if t.get('tax_deductible') else ''} |\\n\")\n\nwith open(csv_path,\"w\",newline=\"\",encoding=\"utf-8\") as f:\n    writer=csv.writer(f)\n    writer.writerow([\"category\",\"total\",\"pct\",\"budget\",\"over_budget\",\"tax_deductible\"])\n    for cat,total in sorted(expenses_only.items(),key=lambda x:-x[1]):\n        pct=total/total_spend*100 if total_spend else 0\n        bgt=budget.get(cat,0)\n        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\"])\n\nwith open(json_path,\"w\",encoding=\"utf-8\") as f:\n    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)\n\nconsole.print()\nconsole.print(Panel(\n    f\"[green]✅ Done![/green]\\n\\n\"\n    f\"📝 [cyan]{md_path}[/cyan]\\n\"\n    f\"📊 [cyan]{csv_path}[/cyan]\\n\"\n    f\"📄 [cyan]{json_path}[/cyan]\",\n    title=\"Exports\", border_style=\"green\"\n))\n```\n","topics":["CSV","Expense Tracking"],"tags":{"latest":"1.0.4","bookkeeping":"1.0.0","budget":"1.0.1","expenses":"1.0.1","finance":"1.0.1","forecasting":"1.0.1","plutus":"1.0.0","premium":"1.0.1","pro":"1.0.1","tax":"1.0.0","tracker":"1.0.0"},"stats":{"comments":0,"downloads":474,"installsAllTime":17,"installsCurrent":0,"stars":0,"versions":3},"createdAt":1778378115374,"updatedAt":1779648900415},"latestVersion":{"version":"1.0.4","createdAt":1779648900415,"changelog":"- Updated all brand and documentation text to display proper Unicode symbols (e.g. emoji, currency signs) instead of mojibake.\n- Added direct purchase links and clearer setup instructions for individual licenses and the Pro skill bundle.\n- Switched license key instructions to updated URLs and emphasized the simplified one-time payment.\n- Improved onboarding section for easier installation, activation, and getting started.","license":"MIT-0"},"metadata":null,"owner":{"handle":"occupythemilkyway","userId":"s1757zbbgb226m951fdfmj4s6s860nkn","displayName":"OccupyTheMilkyWay","image":"https://avatars.githubusercontent.com/u/135929372?v=4"},"moderation":null}