diff --git a/clawhub/deal-desk/LICENSE b/clawhub/deal-desk/LICENSE new file mode 100644 index 0000000..e9bdd70 --- /dev/null +++ b/clawhub/deal-desk/LICENSE @@ -0,0 +1,18 @@ +MIT No Attribution + +Copyright 2026 Solid State / Visionaire Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/clawhub/deal-desk/README.md b/clawhub/deal-desk/README.md new file mode 100644 index 0000000..75f7e36 --- /dev/null +++ b/clawhub/deal-desk/README.md @@ -0,0 +1,44 @@ +# Deal Desk + +A sales CRM that lives in your chat. Track deals through every stage, log the +calls and emails, get a ranked daily priority list, review the whole pipeline, +and forecast revenue — without leaving the conversation or standing up a CRM. + +The skill is a single deterministic Python script (`entry.py`): **JSON in on +stdin, JSON out on stdout.** No network, no credentials, no file writes. Your +agent holds the pipeline state and passes it back on each call, so the data +never leaves your conversation. + +## What it does + +- **Stages** — `lead → qualified → demo → proposal → negotiation → closed-won` (and `closed-lost`), with stage history per deal. +- **Interactions** — log calls, emails, meetings, demos, and notes against a deal, and set the next step in the same move. +- **Daily priorities** — what's overdue or stalled, ranked, with the reason for each. +- **Pipeline review** — count and value by stage, open value, weighted pipeline, win rate, stalled deals. +- **Forecast** — commit / weighted / best-case / won-to-date, optionally scoped to a close-date month. + +## Quick start + +```bash +# First call — no state yet +echo '{"action":"add_deal","today":"2026-06-04","name":"Acme Corp","amount":12000,"stage":"qualified"}' | python3 entry.py +``` + +The response includes a `state` object. Keep it and pass it back as `"state"` +on the next call: + +```bash +echo '{"action":"daily_priorities","today":"2026-06-04","state":{"deals":[...]}}' | python3 entry.py +``` + +See [SKILL.md](./SKILL.md) for the full action and field reference. + +## Runtime + +- Python 3, standard library only. No dependencies to install. +- No network access, no credentials, no file writes. +- Up to 5000 deals per pipeline; 5 MB stdin; 30 s wall time. + +## License + +MIT-0 (MIT No Attribution). See [LICENSE](./LICENSE). diff --git a/clawhub/deal-desk/SKILL.md b/clawhub/deal-desk/SKILL.md new file mode 100644 index 0000000..72b098c --- /dev/null +++ b/clawhub/deal-desk/SKILL.md @@ -0,0 +1,108 @@ +--- +name: deal-desk +slug: deal-desk +version: 1.0.0 +description: Sales pipeline in chat — track deals through stages, log calls, get today's priorities, forecast revenue. Use when the user adds or moves a deal, logs a call, or asks what to work on today. +license: MIT-0 +author: solidstatecc +provenance: first-party +allowed-tools: Bash +runtime: + network: false + credentials: false + writes: false +limits: + max-deals: 5000 + max-bytes-stdin: 5242880 + timeout-seconds: 30 +negative-triggers: + - Not a hosted CRM. It does not store data — the caller owns the JSON state. + - Not for sending email or making calls. It logs that they happened; it does not perform them. + - Not a sync bridge to Salesforce/HubSpot/Pipedrive. No network, ever. + - Not for marketing-campaign automation or lead scraping. +--- + +# deal-desk + +A sales CRM that lives in the conversation. The math is deterministic and runs +in a bundled `entry.py`: deal stages, interaction logs, a daily priority list, +pipeline review, and a weighted revenue forecast. No network, no credentials, +no file writes — the caller holds the state and passes it back each turn. + +## How to run + +Invoke `entry.py` with **Bash**, passing one JSON object on stdin and reading +one JSON object on stdout. Bash is used **only** to execute the bundled script +(`python3 entry.py`) — it makes no network calls, reads no credentials, and +writes no files. Nothing else needs Bash. + +``` +echo '' | python3 entry.py +``` + +State lives in the conversation, not on disk. Keep the `state` object from each +response and pass it back in the next call as `"state"`. On the first call omit +`state` (the script starts from an empty pipeline). Always pass `"today"` as a +`YYYY-MM-DD` string so date math (overdue, stalled, forecast) is stable. + +## Inputs + +Every call is one object: + +```json +{ "action": "", "state": { "deals": [...] }, "today": "2026-06-04", ...args } +``` + +Actions: + +- **add_deal** — `name` (required), `amount`, `stage`, `contact`, `company`, `close_date`, `next_action`, `next_action_date`, `tags`. Returns the new deal (id `d1`, `d2`, …). +- **update_deal** — `deal_id` + any editable field (`name`, `amount`, `close_date`, `next_action`, `tags`, `probability`, …). +- **move_stage** — `deal_id`, `stage`. Records stage history; closing a deal clears its next action and sets `close_date` if empty. +- **log_interaction** — `deal_id`, `type` (`call`/`email`/`meeting`/`demo`/`note`/`other`), `note`, optional `date`, optional `next_action`/`next_action_date`. +- **set_next_action** — `deal_id`, `next_action`, `next_action_date`. +- **list_deals** — optional `filter` (`stage`, `open_only`, `tag`). Sorted by stage then value. +- **daily_priorities** — deals that are overdue or stalled (default `stale_days` 7), ranked with reasons. +- **pipeline_review** — counts and value per stage, open value, weighted pipeline, win rate, stalled deals. +- **forecast** — `commit` / `weighted` / `best_case` / `won_to_date`. Pass `month` (`"YYYY-MM"`) to scope by close date. + +## Stages and probabilities + +`lead` (0.10) → `qualified` (0.25) → `demo` (0.45) → `proposal` (0.65) → +`negotiation` (0.80) → `closed-won` (1.0). `closed-lost` (0.0) is the other +terminal stage. A deal may override its odds with a `probability` field; the +weighted forecast uses it instead of the stage default. + +## Output contract + +Every response is one JSON object. The `ok` field is the verdict: + +- `ok: true` — the action applied. Read `state` (the full updated pipeline — persist it), `result` (the action's data), and `summary` (a one-line recap). +- `ok: false` — nothing changed. Read `error` for the reason and keep the state you already had. + +```json +{ "ok": true, "action": "move_stage", "state": {...}, "result": {...}, "summary": "Moved Acme (d1) from 'qualified' to 'demo'." } +``` + +`list_deals`, `daily_priorities`, `pipeline_review`, and `forecast` are +read-only: they return `state` unchanged. + +## Conversation pattern + +1. User says "add Acme, $12k, just qualified them" → call `add_deal`. +2. User says "logged a call, sending the proposal Friday" → call `log_interaction` with `next_action`/`next_action_date`. +3. Each morning, "what's on my plate?" → call `daily_priorities` and read back the ranked list with reasons. +4. "How's the quarter looking?" → call `forecast` (optionally scoped to a month) and quote commit vs. weighted vs. best-case. + +Surface the `summary` to the user in plain language; keep the full `state` +silently for the next call. + +## Do not use for + +- Persisting data yourself — there is no database. The host owns `state`. +- Sending email, dialing calls, or scheduling meetings. It records that they happened. +- Syncing with an external CRM or scraping leads. No network access exists. + +## Limits + +- Up to 5000 deals per pipeline; 5 MB of stdin. +- Wall time: 30 s. Pure stdlib Python 3 — no dependencies to install. diff --git a/clawhub/deal-desk/entry.py b/clawhub/deal-desk/entry.py new file mode 100644 index 0000000..3a854d1 --- /dev/null +++ b/clawhub/deal-desk/entry.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +"""Deal Desk — a CRM that lives in chat. + +JSON in on stdin, JSON out on stdout. Pure stdlib, no network, no credentials, +no file writes. The caller owns persistence: every response echoes the full +updated `state`, and the caller passes it back on the next call. + +Contract +-------- +Input: {"action": "", "state": {}?, "today": "YYYY-MM-DD"?, ...args} +Output (success): {"ok": true, "action": "...", "state": {...}, "result": {...}, "summary": "..."} +Output (error): {"ok": false, "action": "...", "error": "..."} + +`ok` is the verdict: true = applied, false = nothing changed. On error the +caller should keep the state it already had — this script never mutates input +in place, it returns a fresh copy. +""" + +import sys +import json +import copy +from datetime import date, datetime + +# --- Pipeline definition ---------------------------------------------------- + +# Ordered open stages, then the two terminal stages. +OPEN_STAGES = ["lead", "qualified", "demo", "proposal", "negotiation"] +WON = "closed-won" +LOST = "closed-lost" +STAGES = OPEN_STAGES + [WON, LOST] + +# Win probability per stage. Used for the weighted forecast. These are the +# default ladder; a deal may override with its own "probability" field. +STAGE_PROBABILITY = { + "lead": 0.10, + "qualified": 0.25, + "demo": 0.45, + "proposal": 0.65, + "negotiation": 0.80, + WON: 1.0, + LOST: 0.0, +} + +# A deal with no interaction in this many days is "stalled". +DEFAULT_STALE_DAYS = 7 + +INTERACTION_TYPES = ["call", "email", "meeting", "demo", "note", "other"] + + +# --- Helpers ---------------------------------------------------------------- + +class DealDeskError(Exception): + """Raised for any caller-facing validation failure.""" + + +def parse_date(value, field): + if not isinstance(value, str): + raise DealDeskError("%s must be a YYYY-MM-DD string" % field) + try: + return datetime.strptime(value, "%Y-%m-%d").date() + except ValueError: + raise DealDeskError("%s is not a valid YYYY-MM-DD date: %r" % (field, value)) + + +def today_of(payload): + raw = payload.get("today") + if raw is None: + return date.today() + return parse_date(raw, "today") + + +def empty_state(): + return {"deals": []} + + +def load_state(payload): + state = payload.get("state") + if state is None: + return empty_state() + if not isinstance(state, dict): + raise DealDeskError("state must be an object") + deals = state.get("deals", []) + if not isinstance(deals, list): + raise DealDeskError("state.deals must be a list") + fresh = copy.deepcopy(state) + fresh.setdefault("deals", []) + return fresh + + +def next_deal_id(deals): + """Deterministic sequential id: highest existing dNN + 1, else d1.""" + highest = 0 + for d in deals: + did = str(d.get("id", "")) + if did.startswith("d") and did[1:].isdigit(): + highest = max(highest, int(did[1:])) + return "d%d" % (highest + 1) + + +def find_deal(state, deal_id): + for d in state["deals"]: + if d.get("id") == deal_id: + return d + raise DealDeskError("no deal with id %r" % deal_id) + + +def is_open(deal): + return deal.get("stage") in OPEN_STAGES + + +def deal_probability(deal): + if isinstance(deal.get("probability"), (int, float)): + return float(deal["probability"]) + return STAGE_PROBABILITY.get(deal.get("stage"), 0.0) + + +def amount_of(deal): + amt = deal.get("amount", 0) + return float(amt) if isinstance(amt, (int, float)) else 0.0 + + +def round2(x): + return round(float(x) + 0.0, 2) + + +def days_since(iso, today): + if not iso: + return None + try: + d = datetime.strptime(iso, "%Y-%m-%d").date() + except (ValueError, TypeError): + return None + return (today - d).days + + +def last_interaction_date(deal): + dates = [i.get("date") for i in deal.get("interactions", []) if i.get("date")] + return max(dates) if dates else None + + +# --- Actions ---------------------------------------------------------------- + +def act_add_deal(state, payload, today): + name = payload.get("name") + if not name or not isinstance(name, str): + raise DealDeskError("add_deal requires a non-empty 'name'") + + stage = payload.get("stage", "lead") + if stage not in STAGES: + raise DealDeskError("unknown stage %r; valid: %s" % (stage, ", ".join(STAGES))) + + amount = payload.get("amount", 0) + if not isinstance(amount, (int, float)): + raise DealDeskError("amount must be a number") + + iso_today = today.isoformat() + deal = { + "id": next_deal_id(state["deals"]), + "name": name, + "contact": payload.get("contact", ""), + "company": payload.get("company", ""), + "amount": amount, + "stage": stage, + "created_at": iso_today, + "updated_at": iso_today, + "close_date": payload.get("close_date", ""), + "next_action": payload.get("next_action", ""), + "next_action_date": payload.get("next_action_date", ""), + "interactions": [], + "stage_history": [{"stage": stage, "at": iso_today}], + "tags": payload.get("tags", []) if isinstance(payload.get("tags"), list) else [], + } + if payload.get("close_date"): + parse_date(payload["close_date"], "close_date") + if payload.get("next_action_date"): + parse_date(payload["next_action_date"], "next_action_date") + + state["deals"].append(deal) + return deal, "Added %s (%s) at stage '%s'." % (deal["name"], deal["id"], stage) + + +def act_update_deal(state, payload, today): + deal = find_deal(state, payload.get("deal_id")) + editable = ["name", "contact", "company", "amount", "close_date", + "next_action", "next_action_date", "tags", "probability"] + changed = [] + for key in editable: + if key in payload: + if key == "amount" and not isinstance(payload[key], (int, float)): + raise DealDeskError("amount must be a number") + if key in ("close_date", "next_action_date") and payload[key]: + parse_date(payload[key], key) + deal[key] = payload[key] + changed.append(key) + if not changed: + raise DealDeskError("update_deal: no editable fields supplied") + deal["updated_at"] = today.isoformat() + return deal, "Updated %s on %s: %s." % (deal["id"], deal["name"], ", ".join(changed)) + + +def act_move_stage(state, payload, today): + deal = find_deal(state, payload.get("deal_id")) + stage = payload.get("stage") + if stage not in STAGES: + raise DealDeskError("unknown stage %r; valid: %s" % (stage, ", ".join(STAGES))) + prev = deal.get("stage") + if stage == prev: + raise DealDeskError("deal %s is already at stage '%s'" % (deal["id"], stage)) + iso_today = today.isoformat() + deal["stage"] = stage + deal["updated_at"] = iso_today + deal.setdefault("stage_history", []).append({"stage": stage, "at": iso_today}) + if stage in (WON, LOST): + # Closing clears the open follow-up. + deal["next_action"] = "" + deal["next_action_date"] = "" + if not deal.get("close_date"): + deal["close_date"] = iso_today + return deal, "Moved %s (%s) from '%s' to '%s'." % (deal["name"], deal["id"], prev, stage) + + +def act_log_interaction(state, payload, today): + deal = find_deal(state, payload.get("deal_id")) + itype = payload.get("type", "note") + if itype not in INTERACTION_TYPES: + raise DealDeskError("unknown interaction type %r; valid: %s" + % (itype, ", ".join(INTERACTION_TYPES))) + when = payload.get("date") or today.isoformat() + parse_date(when, "date") + entry = {"date": when, "type": itype, "note": payload.get("note", "")} + deal.setdefault("interactions", []).append(entry) + deal["updated_at"] = today.isoformat() + # Logging an interaction may also set the next step. + if "next_action" in payload: + deal["next_action"] = payload["next_action"] + if "next_action_date" in payload: + if payload["next_action_date"]: + parse_date(payload["next_action_date"], "next_action_date") + deal["next_action_date"] = payload["next_action_date"] + return deal, "Logged %s on %s (%s)." % (itype, deal["name"], when) + + +def act_set_next_action(state, payload, today): + deal = find_deal(state, payload.get("deal_id")) + action = payload.get("next_action", "") + when = payload.get("next_action_date", "") + if when: + parse_date(when, "next_action_date") + deal["next_action"] = action + deal["next_action_date"] = when + deal["updated_at"] = today.isoformat() + return deal, "Next action for %s: %s%s." % ( + deal["name"], action or "(cleared)", (" by %s" % when) if when else "") + + +def act_list_deals(state, payload, today): + deals = list(state["deals"]) + flt = payload.get("filter", {}) or {} + if flt.get("stage"): + deals = [d for d in deals if d.get("stage") == flt["stage"]] + if flt.get("open_only"): + deals = [d for d in deals if is_open(d)] + if flt.get("tag"): + deals = [d for d in deals if flt["tag"] in (d.get("tags") or [])] + # Sort by stage order (open first), then amount desc. + order = {s: i for i, s in enumerate(STAGES)} + deals.sort(key=lambda d: (order.get(d.get("stage"), 99), -amount_of(d))) + rows = [{ + "id": d["id"], "name": d["name"], "stage": d.get("stage"), + "amount": amount_of(d), "next_action": d.get("next_action", ""), + "next_action_date": d.get("next_action_date", ""), + } for d in deals] + return {"deals": rows, "count": len(rows)}, "%d deal(s)." % len(rows) + + +def act_daily_priorities(state, payload, today): + stale_days = payload.get("stale_days", DEFAULT_STALE_DAYS) + iso_today = today.isoformat() + items = [] + for d in state["deals"]: + if not is_open(d): + continue + reasons = [] + score = 0.0 + weight = amount_of(d) * deal_probability(d) + + nad = d.get("next_action_date") + if nad: + overdue = days_since(nad, today) + if overdue is not None and overdue > 0: + reasons.append("overdue %d day(s): %s" % (overdue, d.get("next_action") or "follow up")) + score += 1000 + overdue + elif overdue == 0: + reasons.append("due today: %s" % (d.get("next_action") or "follow up")) + score += 500 + + last = last_interaction_date(d) + idle = days_since(last, today) if last else days_since(d.get("created_at"), today) + if idle is not None and idle >= stale_days: + reasons.append("stalled: %d day(s) since last touch" % idle) + score += 100 + idle + + if not reasons: + continue + score += weight / 1000.0 # tie-break by weighted value + items.append({ + "id": d["id"], "name": d["name"], "stage": d.get("stage"), + "amount": amount_of(d), "weighted": round2(weight), + "next_action": d.get("next_action", ""), + "next_action_date": nad or "", + "reasons": reasons, "_score": score, + }) + items.sort(key=lambda x: -x["_score"]) + for x in items: + del x["_score"] + summary = "%d deal(s) need attention on %s." % (len(items), iso_today) + return {"date": iso_today, "priorities": items, "count": len(items)}, summary + + +def act_pipeline_review(state, payload, today): + by_stage = {} + for s in STAGES: + by_stage[s] = {"count": 0, "value": 0.0} + open_value = 0.0 + weighted = 0.0 + stale_days = payload.get("stale_days", DEFAULT_STALE_DAYS) + stalled = [] + for d in state["deals"]: + stage = d.get("stage") + amt = amount_of(d) + if stage in by_stage: + by_stage[stage]["count"] += 1 + by_stage[stage]["value"] = round2(by_stage[stage]["value"] + amt) + if is_open(d): + open_value += amt + weighted += amt * deal_probability(d) + last = last_interaction_date(d) or d.get("created_at") + idle = days_since(last, today) + if idle is not None and idle >= stale_days: + stalled.append({"id": d["id"], "name": d["name"], + "stage": stage, "idle_days": idle, "amount": amt}) + won = by_stage[WON]["count"] + lost = by_stage[LOST]["count"] + closed = won + lost + win_rate = round2(won / closed) if closed else None + stalled.sort(key=lambda x: -x["idle_days"]) + result = { + "by_stage": by_stage, + "open_count": sum(by_stage[s]["count"] for s in OPEN_STAGES), + "open_value": round2(open_value), + "weighted_pipeline": round2(weighted), + "won_count": won, "lost_count": lost, "win_rate": win_rate, + "stalled": stalled, + } + summary = ("%d open deal(s), $%s in pipeline, $%s weighted; %d stalled." + % (result["open_count"], result["open_value"], + result["weighted_pipeline"], len(stalled))) + return result, summary + + +def act_forecast(state, payload, today): + """Weighted / commit / best-case forecast, optionally for a target month.""" + month = payload.get("month") # "YYYY-MM" to scope by close_date + weighted = 0.0 + best_case = 0.0 + commit = 0.0 + won_to_date = 0.0 + scoped = [] + for d in state["deals"]: + stage = d.get("stage") + amt = amount_of(d) + cd = d.get("close_date") or "" + if month and not cd.startswith(month): + continue + if stage == WON: + won_to_date += amt + continue + if stage == LOST: + continue + weighted += amt * deal_probability(d) + best_case += amt + if stage == "negotiation": + commit += amt + scoped.append(d["id"]) + result = { + "month": month or "all-open", + "commit": round2(commit), + "weighted": round2(weighted), + "best_case": round2(best_case), + "won_to_date": round2(won_to_date), + "deal_count": len(scoped), + } + summary = ("Forecast (%s): commit $%s / weighted $%s / best-case $%s; won $%s." + % (result["month"], result["commit"], result["weighted"], + result["best_case"], result["won_to_date"])) + return result, summary + + +ACTIONS = { + "add_deal": act_add_deal, + "update_deal": act_update_deal, + "move_stage": act_move_stage, + "log_interaction": act_log_interaction, + "set_next_action": act_set_next_action, + "list_deals": act_list_deals, + "daily_priorities": act_daily_priorities, + "pipeline_review": act_pipeline_review, + "forecast": act_forecast, +} + +# Actions that read but never change state — their returned state is unchanged. +READ_ONLY = {"list_deals", "daily_priorities", "pipeline_review", "forecast"} + + +def handle(payload): + if not isinstance(payload, dict): + raise DealDeskError("input must be a JSON object") + action = payload.get("action") + if action not in ACTIONS: + raise DealDeskError("unknown action %r; valid: %s" + % (action, ", ".join(sorted(ACTIONS)))) + today = today_of(payload) + state = load_state(payload) + out = ACTIONS[action](state, payload, today) + result, summary = out + return {"ok": True, "action": action, "state": state, + "result": result, "summary": summary} + + +def main(): + raw = sys.stdin.read() + action = None + try: + payload = json.loads(raw) if raw.strip() else {} + action = payload.get("action") if isinstance(payload, dict) else None + response = handle(payload) + except json.JSONDecodeError as exc: + response = {"ok": False, "action": None, "error": "invalid JSON: %s" % exc} + except DealDeskError as exc: + response = {"ok": False, "action": action, "error": str(exc)} + sys.stdout.write(json.dumps(response, ensure_ascii=False)) + sys.stdout.write("\n") + + +if __name__ == "__main__": + main() diff --git a/lib/skills.ts b/lib/skills.ts index 9fc094d..a9b1ea0 100644 --- a/lib/skills.ts +++ b/lib/skills.ts @@ -131,6 +131,33 @@ Live on [ClawHub](https://clawhub.ai/solidstate/publish-audit). Free, MIT-0.`, tags: ["audit", "publish", "clawhub", "trust", "meta"], createdAt: "2026-06-04", }, + { + id: "deal-desk", + name: "Deal Desk", + slug: "deal-desk", + kind: "original", + description: + "A sales CRM that lives in your chat. Track deals through stages, log calls, get a ranked daily priority list, review pipeline, and forecast revenue.", + longDescription: `Deal Desk runs a real sales pipeline from the conversation — no CRM tab, no login, no data leaving the chat. + +**Stages:** lead → qualified → demo → proposal → negotiation → closed-won (and closed-lost), with stage history on every deal. + +**What it does:** add and move deals, log calls / emails / meetings, set the next action, get a daily priority list (what's overdue or stalled, ranked with reasons), review the whole pipeline (value by stage, weighted pipeline, win rate), and forecast revenue (commit / weighted / best-case, scoped to a month). + +The engine is a single deterministic Python script — JSON in, JSON out. No network, no credentials, no file writes. Your agent holds the pipeline state and passes it back each turn, so the math is reproducible and the data stays in your conversation. + +Pay-once, no subscription. Queued for the Solid State audit → test gate before it lists on Claw Mart.`, + author: "solidstatecc", + version: "1.0.0", + platforms: ["claude", "openclaw", "generic"], + categories: ["Sales", "Productivity"], + license: "MIT-0", + status: "alpha", + provenance: "first-party", + featured: true, + tags: ["sales", "crm", "pipeline", "forecast", "deals"], + createdAt: "2026-06-04", + }, ] // ---------------------------------------------------------------------------