Skill: nexaiguy/nex-meetcost (version 1.0.0)

--- name: Nex MeetCost description: Meeting cost calculator. See what meetings actually cost in billable time. Per-attendee rates by role, recurring meeting projections (weekly/monthly/yearly), cost-per-type breakdowns. Python stdlib only, SQLite storage. version: 1.0.0 metadata: author: Nex AI (Kevin Blancaflor) license: MIT-0 website: https://nex-ai.be clawdbot: keywords: - meeting cost - meeting calculator - time wasted - meeting ROI - hourly rate - meeting expense - recurring meeting - standup cost - meeting budget - vergaderkosten - meeting tijd - vergadering kost triggers: - what does this meeting cost - calculate meeting cost - how expensive is this meeting - meeting cost calculator - recurring meeting projection - wat kost deze vergadering - vergaderkosten berekenen --- # Nex MeetCost Meeting cost calculator. See what your meetings actually cost based on who attends and their hourly rates. Track recurring meetings and project monthly/yearly costs. ## Requirements - Python 3.8+ - No external dependencies (stdlib only) - SQLite (built into Python) ## Setup ```bash bash setup.sh ``` ## Commands | Command | What it does | |---------|-------------| | `calc` | Calculate cost (optional save) | | `log` | Log a meeting with cost | | `show` | Show meeting details | | `list` | List logged meetings | | `rates` | Show default hourly rates | | `stats` | Cost statistics and breakdowns | | `export` | Export to JSON or CSV | ## Attendee Format `name:role:rate` separated by commas. Role and rate are optional. - `Kevin:developer` uses the default developer rate - `Kevin:developer:95` uses a custom rate - `Kevin` uses the default "other" rate ## Tone Guide **Quick calculation:** > "What does a 1-hour meeting with 3 developers and a manager cost?" ```bash nex-meetcost calc 60 -a "Kevin:developer,Sarah:developer,Thomas:developer,Lisa:manager" ``` **Log a meeting:** > "Log today's 30-minute standup" ```bash nex-meetcost log "Daily standup" 30 -a "Kevin:developer,Sarah:designer,Thomas:developer" --type standup --recurring 5 ``` **See what recurring meetings cost per year:** > "How much do our recurring meetings cost?" ```bash nex-meetcost stats ``` ## Storage All data stored locally in `~/.nex-meetcost/meetcost.db` (SQLite). No cloud, no telemetry. ## License MIT-0 on ClawHub (free for any use). AGPL-3.0 on GitHub (commercial licenses via [REDACTED_SECRET]).

path: lib/config.py
content: """
Nex MeetCost - Configuration
Copyright 2026 Nex AI (Kevin Blancaflor)
"""
import os
from pathlib import Path

# Paths
DATA_DIR = Path(os.environ.get("NEX_MEETCOST_DIR", Path.home() / ".nex-meetcost"))
DB_PATH = DATA_DIR / "meetcost.db"
EXPORT_DIR = DATA_DIR / "exports"

# Default hourly rates per role
DEFAULT_RATES = {
    "developer": 95.00,
    "designer": 90.00,
    "manager": 110.00,
    "sales": 85.00,
    "support": 70.00,
    "intern": 35.00,
    "executive": 150.00,
    "consultant": 120.00,
    "other": 85.00,
}

# Meeting types
MEETING_TYPES = [
    "standup", "planning", "review", "retro", "client",
    "sales", "internal", "onboarding", "brainstorm", "other",
]

CURRENCY_SYMBOL = "\u20ac"

# Display
SEPARATOR = "=" * 60
SUBSEPARATOR = "-" * 60


path: lib/storage.py
content: """
Nex MeetCost - SQLite storage layer
Copyright 2026 Nex AI (Kevin Blancaflor)
"""
import os
import stat
import json
import sqlite3
import platform
import datetime as dt
from pathlib import Path
from contextlib import contextmanager

from lib.config import (
    DATA_DIR, DB_PATH, EXPORT_DIR,
    DEFAULT_RATES, CURRENCY_SYMBOL,
)


@contextmanager
def _connect():
    conn = sqlite3.connect(str(DB_PATH))
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    conn.execute("PRAGMA journal_mode = WAL")
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()


def init_db():
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    EXPORT_DIR.mkdir(parents=True, exist_ok=True)

    if platform.system() != "Windows":
        try:
            os.chmod(str(DATA_DIR), stat.S_IRWXU)
        except OSError:
            pass

    with _connect() as conn:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS meetings (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                meeting_type TEXT DEFAULT 'other',
                meeting_date TEXT,
                duration_minutes REAL NOT NULL,
                attendees TEXT,
                total_cost REAL,
                notes TEXT,
                recurring INTEGER DEFAULT 0,
                recurrence_weekly INTEGER DEFAULT 0,
                created_at TEXT DEFAULT (datetime('now'))
            );

            CREATE TABLE IF NOT EXISTS attendees (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                meeting_id INTEGER NOT NULL,
                name TEXT NOT NULL,
                role TEXT DEFAULT 'other',
                hourly_rate REAL,
                FOREIGN KEY (meeting_id) REFERENCES meetings(id) ON DELETE CASCADE
            );

            CREATE INDEX IF NOT EXISTS idx_meetings_date ON meetings(meeting_date);
            CREATE INDEX IF NOT EXISTS idx_meetings_type ON meetings(meeting_type);
            CREATE INDEX IF NOT EXISTS idx_attendees_meeting ON attendees(meeting_id);
        """)


def calculate_cost(duration_minutes, attendee_list):
    total = 0.0
    details = []
    for a in attendee_list:
        rate = a.get('rate') if a.get('rate') is not None else DEFAULT_RATES.get(a.get('role', 'other'), 85.0)
        cost = (duration_minutes / 60.0) * rate
        details.append({
            'name': a['name'],
            'role': a.get('role', 'other'),
            'rate': rate,
            'cost': round(cost, 2),
        })
        total += cost
    return round(total, 2), details


def save_meeting(title, duration_minutes, attendee_list, meeting_type="other",
                 meeting_date=None, notes=None, recurring=False, recurrence_weekly=0):
    if not meeting_date:
        meeting_date = dt.date.today().isoformat()

    total_cost, details = calculate_cost(duration_minutes, attendee_list)

    with _connect() as conn:
        cursor = conn.cursor()
        cursor.execute("""
            INSERT INTO meetings (title, meeting_type, meeting_date, duration_minutes,
                                  attendees, total_cost, notes, recurring, recurrence_weekly)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (title, meeting_type, meeting_date, duration_minutes,
              json.dumps(attendee_list), total_cost, notes,
              1 if recurring else 0, recurrence_weekly))
        meeting_id = cursor.lastrowid

        for d in details:
            cursor.execute("""
                INSERT INTO attendees (meeting_id, name, role, hourly_rate)
                VALUES (?, ?, ?, ?)
            """, (meeting_id, d['name'], d['role'], d['rate']))

        return meeting_id, total_cost, details


def get_meeting(meeting_id):
    with _connect() as conn:
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM meetings WHERE id = ?", (meeting_id,))
        row = cursor.fetchone()
        if not row:
            return None

        meeting = dict(row)
        cursor.execute("SELECT * FROM attendees WHERE meeting_id = ?", (meeting_id,))
        meeting['attendee_details'] = [dict(a) for a in cursor.fetchall()]
        return meeting


def list_meetings(meeting_type=None, date_from=None, date_to=None, limit=50):
    with _connect() as conn:
        query = "SELECT * FROM meetings WHERE 1=1"
        params = []

        if meeting_type:
            query += " AND meeting_type = ?"
            params.append(meeting_type)
        if date_from:
            query += " AND meeting_date >= ?"
            params.append(date_from)
        if date_to:
            query += " AND meeting_date <= ?"
            params.append(date_to)

        query += " ORDER BY meeting_date DESC, created_at DESC LIMIT ?"
        params.append(limit)

        cursor = conn.cursor()
        cursor.execute(query, params)
        return [dict(row) for row in cursor.fetchall()]


def get_stats(date_from=None, date_to=None):
    with _connect() as conn:
        cursor = conn.cursor()

        where = "WHERE 1=1"
        params = []
        if date_from:
            where += " AND meeting_date >= ?"
            params.append(date_from)
        if date_to:
            where += " AND meeting_date <= ?"
            params.append(date_to)

        cursor.execute(f"SELECT COUNT(*) as c FROM meetings {where}", params)
        total = cursor.fetchone()['c']

        cursor.execute(f"SELECT COALESCE(SUM(total_cost), 0) as t FROM meetings {where}", params)
        total_cost = cursor.fetchone()['t']

        cursor.execute(f"SELECT COALESCE(SUM(duration_minutes), 0) as t FROM meetings {where}", params)
        total_minutes = cursor.fetchone()['t']

        cursor.execute(f"""
            SELECT meeting_type, COUNT(*) as c, COALESCE(SUM(total_cost), 0) as cost,
                   COALESCE(SUM(duration_minutes), 0) as mins
            FROM meetings {where}
            GROUP BY meeting_type ORDER BY cost DESC
        """, params)
        by_type = {row['meeting_type']: {
            'count': row['c'], 'cost': round(row['cost'], 2), 'minutes': row['mins']
        } for row in cursor.fetchall()}

        # Recurring cost projection (monthly)
        cursor.execute("""
            SELECT COALESCE(SUM(total_cost * recurrence_weekly * 4.33), 0) as monthly
            FROM meetings WHERE recurring = 1
        """)
        monthly_recurring = round(cursor.fetchone()['monthly'], 2)

        cursor.execute(f"""
            SELECT strftime('%Y-%m', meeting_date) as month,
                   COUNT(*) as c, COALESCE(SUM(total_cost), 0) as cost
            FROM meetings {where} AND meeting_date IS NOT NULL
            GROUP BY month ORDER BY month DESC LIMIT 12
        """, params)
        by_month = {row['month']: {
            'count': row['c'], 'cost': round(row['cost'], 2)
        } for row in cursor.fetchall()}

        return {
            'total_meetings': total,
            'total_cost': round(total_cost, 2),
            'total_minutes': total_minutes,
            'total_hours': round(total_minutes / 60.0, 2),
            'avg_cost': round(total_cost / total, 2) if total > 0 else 0,
            'monthly_recurring': monthly_recurring,
            'by_type': by_type,
            'by_month': by_month,
        }


def export_meetings(format_type='json', **filters):
    meetings = list_meetings(limit=10000, **filters)
    if format_type == 'json':
        return json.dumps(meetings, indent=2, default=str)
    elif format_type == 'csv':
        import csv
        import io
        if not meetings:
            return ""
        output = io.StringIO()
        writer = csv.DictWriter(output, fieldnames=meetings[0].keys())
        writer.writeheader()
        writer.writerows(meetings)
        return output.getvalue()
    return ""


path: lib/__init__.py
content: # Nex MeetCost - lib package


path: LICENSE.txt
content: GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007

Copyright (C) 2026 Nex AI (nex-ai.be)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.

ADDITIONAL TERMS:

Commercial License Available
-----------------------------
If you wish to use this software in a proprietary product, offer it as
a hosted service, or integrate it into commercial software without
complying with the AGPL-3.0 obligations (including source disclosure),
a commercial license is available from Nex AI.

Contact: [REDACTED_SECRET]
Website: https://nex-ai.be

DUAL LICENSE NOTICE:
This software is also published on ClawHub under the MIT-0 license as
required by the ClawHub platform. The MIT-0 license applies exclusively
to copies obtained through ClawHub. All copies obtained from this GitHub
repository or any other source are governed by the AGPL-3.0 license above.


path: nex-meetcost.py
content: #!/usr/bin/env python3
"""
Nex MeetCost - Meeting cost calculator.
Copyright 2026 Nex AI (Kevin Blancaflor)
"""
import sys
import os
import json
import argparse
import datetime as dt

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from lib.storage import (
    init_db, calculate_cost, save_meeting, get_meeting,
    list_meetings, get_stats, export_meetings,
)
from lib.config import (
    MEETING_TYPES, DEFAULT_RATES, CURRENCY_SYMBOL,
    SEPARATOR, SUBSEPARATOR, EXPORT_DIR,
)

FOOTER = "[MeetCost by Nex AI | nex-ai.be]"


# --- Helpers ---

def _fmt_money(amount):
    return f"{CURRENCY_SYMBOL}{amount:,.2f}"


def _fmt_duration(minutes):
    h = int(minutes // 60)
    m = int(minutes % 60)
    if h > 0 and m > 0:
        return f"{h}h {m}m"
    if h > 0:
        return f"{h}h"
    return f"{m}m"


def _fmt_date(iso_str):
    if not iso_str:
        return "N/A"
    try:
        return dt.date.fromisoformat(iso_str[:10]).strftime("%Y-%m-%d")
    except (ValueError, TypeError):
        return iso_str


def _parse_attendees(raw):
    """Parse attendees from 'name:role:rate,name:role,name' format."""
    result = []
    for part in raw.split(','):
        parts = part.strip().split(':')
        entry = {'name': parts[0].strip()}
        if len(parts) > 1 and parts[1].strip():
            entry['role'] = parts[1].strip()
        if len(parts) > 2 and parts[2].strip():
            try:
                entry['rate'] = float(parts[2].strip())
            except ValueError:
                pass
        result.append(entry)
    return result


# --- Commands ---

def cmd_calc(args):
    init_db()

    attendee_list = _parse_attendees(args.attendees)
    total, details = calculate_cost(args.duration, attendee_list)

    print(f"\n{SEPARATOR}")
    print(f"MEETING COST: {args.title or 'Quick calculation'}")
    print(f"{SEPARATOR}\n")

    print(f"Duration: {_fmt_duration(args.duration)}")
    print(f"Attendees: {len(details)}\n")

    for d in details:
        print(f"  {d['name']:<20} {d['role']:<12} {_fmt_money(d['rate'])}/h  = {_fmt_money(d['cost'])}")

    print(f"\n{SUBSEPARATOR}")
    print(f"TOTAL COST: {_fmt_money(total)}")

    if args.recurring:
        weekly = total * args.recurring
        monthly = weekly * 4.33
        yearly = weekly * 52
        print(f"\nRecurring {args.recurring}x/week:")
        print(f"  Weekly:  {_fmt_money(weekly)}")
        print(f"  Monthly: {_fmt_money(monthly)}")
        print(f"  Yearly:  {_fmt_money(yearly)}")

    print(f"\n{FOOTER}")

    if args.save:
        meeting_id, _, _ = save_meeting(
            title=args.title or "Untitled meeting",
            duration_minutes=args.duration,
            attendee_list=attendee_list,
            meeting_type=args.type,
            notes=args.notes,
            recurring=args.recurring is not None and args.recurring > 0,
            recurrence_weekly=args.recurring or 0,
        )
        print(f"Saved as meeting #{meeting_id}")


def cmd_log(args):
    init_db()

    attendee_list = _parse_attendees(args.attendees)

    meeting_id, total_cost, details = save_meeting(
        title=args.title,
        duration_minutes=args.duration,
        attendee_list=attendee_list,
        meeting_type=args.type,
        meeting_date=args.date,
        notes=args.notes,
        recurring=args.recurring is not None and args.recurring > 0,
        recurrence_weekly=args.recurring or 0,
    )

    print(f"\nMeeting logged (ID: {meeting_id})")
    print(f"  Title: {args.title}")
    print(f"  Duration: {_fmt_duration(args.duration)}")
    print(f"  Attendees: {len(details)}")
    print(f"  Total cost: {_fmt_money(total_cost)}")

    if args.recurring:
        monthly = total_cost * args.recurring * 4.33
        print(f"  Recurring: {args.recurring}x/week ({_fmt_money(monthly)}/month)")

    print(FOOTER)


def cmd_show(args):
    init_db()

    meeting = get_meeting(args.id)
    if not meeting:
        print(f"Meeting {args.id} not found.")
        print(FOOTER)
        return

    print(f"\n{SEPARATOR}")
    print(f"MEETING #{meeting['id']}: {meeting['title']}")
    print(f"{SEPARATOR}\n")

    print(f"Type: {meeting['meeting_type']}")
    print(f"Date: {_fmt_date(meeting['meeting_date'])}")
    print(f"Duration: {_fmt_duration(meeting['duration_minutes'])}")
    print(f"Total cost: {_fmt_money(meeting['total_cost'])}")

    if meeting['recurring']:
        weekly = meeting['total_cost'] * meeting['recurrence_weekly']
        monthly = weekly * 4.33
        print(f"Recurring: {meeting['recurrence_weekly']}x/week ({_fmt_money(monthly)}/month)")

    if meeting['attendee_details']:
        print(f"\nAttendees:")
        for a in meeting['attendee_details']:
            cost = (meeting['duration_minutes'] / 60.0) * (a['hourly_rate'] or 0)
            print(f"  {a['name']:<20} {a['role']:<12} {_fmt_money(a['hourly_rate'] or 0)}/h  = {_fmt_money(cost)}")

    if meeting['notes']:
        print(f"\nNotes: {meeting['notes']}")

    print(f"\n{FOOTER}")


def cmd_list(args):
    init_db()

    meetings = list_meetings(
        meeting_type=args.type,
        date_from=args.date_from,
        date_to=args.date_to,
        limit=args.limit or 50,
    )

    if not meetings:
        print("No meetings logged.")
        print(FOOTER)
        return

    print(f"\n{'ID':<5} {'Date':<12} {'Duration':<10} {'Cost':<12} {'Title':<25} {'Type':<10}")
    print("-" * 74)

    total_cost = 0
    for m in meetings:
        title = m['title'][:24]
        date = _fmt_date(m['meeting_date'])
        dur = _fmt_duration(m['duration_minutes'])
        cost = _fmt_money(m['total_cost'])
        total_cost += m['total_cost']
        print(f"{m['id']:<5} {date:<12} {dur:<10} {cost:<12} {title:<25} {m['meeting_type']:<10}")

    print(f"\nTotal: {len(meetings)} meetings | {_fmt_money(total_cost)}")
    print(FOOTER)


def cmd_rates(args):
    print(f"\nDefault hourly rates:\n")
    for role, rate in sorted(DEFAULT_RATES.items()):
        print(f"  {role:<16} {_fmt_money(rate)}/h")
    print(f"\nOverride per attendee: name:role:rate")
    print(FOOTER)


def cmd_stats(args):
    init_db()

    stats = get_stats(date_from=args.date_from, date_to=args.date_to)

    print(f"\n{SEPARATOR}")
    print(f"MEETCOST STATISTICS")
    print(f"{SEPARATOR}\n")

    print(f"Total meetings: {stats['total_meetings']}")
    print(f"Total time in meetings: {_fmt_duration(stats['total_minutes'])} ({stats['total_hours']}h)")
    print(f"Total cost: {_fmt_money(stats['total_cost'])}")
    print(f"Average cost per meeting: {_fmt_money(stats['avg_cost'])}")

    if stats['monthly_recurring'] > 0:
        print(f"Monthly recurring cost: {_fmt_money(stats['monthly_recurring'])}")
        print(f"Yearly recurring cost: {_fmt_money(stats['monthly_recurring'] * 12)}")

    if stats['by_type']:
        print(f"\nBy Meeting Type:")
        for mtype, data in stats['by_type'].items():
            print(f"  {mtype:<16} {data['count']} meetings  {_fmt_duration(data['minutes']):<10} {_fmt_money(data['cost'])}")

    if stats['by_month']:
        print(f"\nCost per Month:")
        for month, data in stats['by_month'].items():
            bar = '#' * max(1, int(data['cost'] / 50))
            print(f"  {month} {bar} {_fmt_money(data['cost'])} ({data['count']})")

    print(f"\n{FOOTER}")


def cmd_export(args):
    init_db()

    data = export_meetings(format_type=args.format)
    if not data:
        print("No meetings to export.")
        return

    output_file = args.output or f"meetcost_export.{args.format}"
    output_path = EXPORT_DIR / output_file

    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(data)

    print(f"Exported to {output_path}")
    print(FOOTER)


# --- Main ---

def main():
    parser = argparse.ArgumentParser(
        description="Nex MeetCost - Meeting cost calculator.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    subparsers = parser.add_subparsers(dest='command', help='Commands')

    # CALC
    p = subparsers.add_parser('calc', help='Calculate meeting cost (optional save)')
    p.add_argument('duration', type=float, help='Duration in minutes')
    p.add_argument('--attendees', '-a', required=True, help='Attendees: name:role:rate,name:role')
    p.add_argument('--title', '-t', help='Meeting title')
    p.add_argument('--type', default='other', choices=MEETING_TYPES, help='Meeting type')
    p.add_argument('--recurring', type=int, help='Times per week (for projection)')
    p.add_argument('--notes', help='Notes')
    p.add_argument('--save', action='store_true', help='Save to database')
    p.set_defaults(func=cmd_calc)

    # LOG
    p = subparsers.add_parser('log', help='Log a meeting with cost')
    p.add_argument('title', help='Meeting title')
    p.add_argument('duration', type=float, help='Duration in minutes')
    p.add_argument('--attendees', '-a', required=True, help='Attendees: name:role:rate,name:role')
    p.add_argument('--type', default='other', choices=MEETING_TYPES, help='Meeting type')
    p.add_argument('--date', help='Meeting date (YYYY-MM-DD)')
    p.add_argument('--recurring', type=int, help='Times per week')
    p.add_argument('--notes', help='Notes')
    p.set_defaults(func=cmd_log)

    # SHOW
    p = subparsers.add_parser('show', help='Show meeting details')
    p.add_argument('id', type=int, help='Meeting ID')
    p.set_defaults(func=cmd_show)

    # LIST
    p = subparsers.add_parser('list', help='List meetings')
    p.add_argument('--type', choices=MEETING_TYPES, help='Filter by type')
    p.add_argument('--date-from', help='From date (YYYY-MM-DD)')
    p.add_argument('--date-to', help='To date (YYYY-MM-DD)')
    p.add_argument('--limit', type=int, default=50, help='Max results')
    p.set_defaults(func=cmd_list)

    # RATES
    p = subparsers.add_parser('rates', help='Show default hourly rates')
    p.set_defaults(func=cmd_rates)

    # STATS
    p = subparsers.add_parser('stats', help='Show statistics')
    p.add_argument('--date-from', help='From date')
    p.add_argument('--date-to', help='To date')
    p.set_defaults(func=cmd_stats)

    # EXPORT
    p = subparsers.add_parser('export', help='Export meetings')
    p.add_argument('format', choices=['json', 'csv'], help='Export format')
    p.add_argument('--output', help='Output filename')
    p.set_defaults(func=cmd_export)

    args = parser.parse_args()

    if not hasattr(args, 'func'):
        parser.print_help()
        return

    try:
        args.func(args)
    except KeyboardInterrupt:
        print("\nInterrupted.")
        sys.exit(130)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


path: README.md
content: # Nex MeetCost

Meeting cost calculator. See what your meetings actually cost based on attendees and hourly rates. Track recurring meetings and project monthly/yearly costs.

**Built by [Nex AI](https://nex-ai.be)**

## Features

- Per-attendee cost calculation with role-based rates
- 9 default role rates (developer, designer, manager, etc.)
- Recurring meeting projections (weekly/monthly/yearly)
- Cost breakdowns by meeting type
- Log and track all meetings
- Export to JSON or CSV
- Python stdlib only, SQLite storage, zero dependencies

## Setup

```bash
bash setup.sh
```

## Usage

```bash
# Quick calculation
nex-meetcost calc 60 -a "Kevin:developer,Sarah:designer,Lisa:manager"

# Log a recurring standup
nex-meetcost log "Daily standup" 15 -a "Kevin:developer,Sarah:developer,Thomas:designer" --type standup --recurring 5

# See total meeting costs
nex-meetcost stats

# Show default rates
nex-meetcost rates
```

## License

- **ClawHub:** MIT-0 (free for any use)
- **GitHub:** AGPL-3.0 (commercial licenses available via [REDACTED_SECRET])


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

SKILL_NAME="Nex MeetCost"
SKILL_SLUG="nex-meetcost"
BRAND="Nex AI (nex-ai.be)"
DATA_DIR="$HOME/.nex-meetcost"
BIN_DIR="$HOME/.local/bin"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

echo "=========================================="
echo "  $SKILL_NAME - Setup"
echo "  By $BRAND"
echo "=========================================="
echo ""

PYTHON=""
for cmd in python3 python; do
    if command -v "$cmd" &>/dev/null; then
        version=$("$cmd" --version 2>&1 | grep -oP '\d+\.\d+\.\d+')
        major=$(echo "$version" | cut -d. -f1)
        minor=$(echo "$version" | cut -d. -f2)
        if [ "$major" -ge 3 ] && [ "$minor" -ge 8 ]; then
            PYTHON="$cmd"
            echo "[OK] Found Python $version"
            break
        fi
    fi
done

if [ -z "$PYTHON" ]; then
    echo "[ERROR] Python 3.8+ required but not found."
    exit 1
fi

mkdir -p "$DATA_DIR"
echo "[OK] Created: $DATA_DIR"
mkdir -p "$DATA_DIR/exports"
echo "[OK] Created: $DATA_DIR/exports"

if [[ "$(uname)" != MINGW* ]] && [[ "$(uname)" != MSYS* ]]; then
    chmod 700 "$DATA_DIR" 2>/dev/null || true
fi

echo ""
echo "Initializing database..."
$PYTHON -c "
import sys
sys.path.insert(0, '$SCRIPT_DIR')
from lib.storage import init_db
init_db()
print('Database initialized.')
"
echo "[OK] Database ready at $DATA_DIR/meetcost.db"

echo ""
echo "Verifying CLI..."
$PYTHON "$SCRIPT_DIR/$SKILL_SLUG.py" --help > /dev/null 2>&1
echo "[OK] CLI verified"

mkdir -p "$BIN_DIR"
WRAPPER="$BIN_DIR/$SKILL_SLUG"

cat > "$WRAPPER" << WRAPPER_EOF
#!/usr/bin/env bash
exec $PYTHON "$SCRIPT_DIR/$SKILL_SLUG.py" "\$@"
WRAPPER_EOF

chmod +x "$WRAPPER"
echo "[OK] Created CLI wrapper at $WRAPPER"

if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then
    echo "[NOTE] Add $BIN_DIR to your PATH:"
    echo "  export PATH=\"\$HOME/.local/bin:\$PATH\""
fi

echo ""
echo "=========================================="
echo "  Setup complete!"
echo "=========================================="
echo ""
echo "Quick start:"
echo "  $SKILL_SLUG calc 60 -a \"Kevin:developer,Sarah:designer\""
echo "  $SKILL_SLUG log \"Standup\" 15 -a \"Kevin:developer\" --type standup"
echo "  $SKILL_SLUG stats"
echo ""
echo "Data stored at: $DATA_DIR"
echo "No telemetry. No tracking. Your data is yours."
echo ""
echo "[$SKILL_NAME by $BRAND]"


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