From 34a0d73d00eb160c68587b3d0815cb13e76d6ccf Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Wed, 20 May 2026 16:43:23 -0700 Subject: [PATCH] syncro: post a summary + link to #bot-alerts after every write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add .claude/scripts/post-bot-alert.sh — reusable, soft-failing Discord poster that reads the bot token from the SOPS vault (bot-token.sops.yaml, credentials.bot_token) with a .env fallback, so it works from any machine. Wire it into the /syncro skill: a Hard Rules pointer, a billing-workflow step (17), and a "Post to #bot-alerts" reference section with the message format and ticket/invoice/customer link mapping (computerguru.syncromsp.com). Scoped to write ops (create/update/close/comment/bill/customer); reads post nothing. Best-effort — never fails the Syncro write it follows. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/commands/syncro.md | 67 +++++++++++++++++++++++++++++++ .claude/scripts/post-bot-alert.sh | 64 +++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 .claude/scripts/post-bot-alert.sh diff --git a/.claude/commands/syncro.md b/.claude/commands/syncro.md index 2c0ea65..3be028d 100644 --- a/.claude/commands/syncro.md +++ b/.claude/commands/syncro.md @@ -48,6 +48,8 @@ Create, update, close, comment on, and bill tickets in Syncro PSA. **Always pass `"taxable": false` explicitly on labor line items.** Labor products are configured with `taxable: false` in Syncro, but `add_line_item` via API does not inherit the product's taxable setting — it posts the line item as `taxable: true` regardless. Always include `"taxable": false` in the payload to match the product's configured value. +**After every write operation, post a summary + link to #bot-alerts.** Every ticket created, updated, closed, or commented, every billing run, and every customer created posts a one-line alert to the team's live feed in Discord. This runs AFTER the write succeeds (never before — no alert for an action that didn't happen) and applies regardless of who runs the skill or where (workstation or the Discord bot). Read-only commands (list / view / search) post nothing. Full format, link mapping, and helper call are in "Post to #bot-alerts" below. + ## Implementation When invoked, use the Syncro REST API via `curl`. All requests include `?api_key=` as query parameter (NOT in header — Syncro uses query param auth). @@ -728,6 +730,11 @@ When `/syncro bill ` is called: 14. Create invoice: `POST /invoices` with `{"ticket_id": N, "customer_id": N, "category": "Standard"}` 15. Verify invoice: `GET /invoices/{id}` → confirm line items transferred. **For prepaid customers, `.invoice.total` will be $0.00 — this is correct.** The line item name is annotated "- Applied X Prepay Hours" and the block is debited. Confirm by re-fetching `customer.prepay_hours` and checking it dropped by `quantity`. For non-prepaid customers, `.invoice.total` must equal `qty × price_retail`. 16. Update ticket status to `Invoiced` +17. **Post to #bot-alerts** (see "Post to #bot-alerts" below): one line linking the ticket, with the invoice total (note the prepay deduction if the customer is prepaid). Example: + ```bash + bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \ + "[SYNCRO] Mike billed #32164 (Jerry Burger) — 1.0h remote, \$150.00 invoiced → https://computerguru.syncromsp.com/tickets/${TICKET_DB_ID}" + ``` **If `.invoice.total` comes back $0.00** (auto-generated line item went in with null price and you missed step 13): `PUT /tickets/{id}/update_line_item` with `price_retail` on each item, then `DELETE /invoices/{bad_id}` and re-POST `/invoices`. Recovery verified on #32203 (2026-04-23). @@ -821,3 +828,63 @@ The two heredocs that interpolate `${TIMER_ID}` / `${LINE_ID}` / `${ID}` / `${CU ### Integration with session logs When closing a ticket (`/syncro close`), offer to create a session log entry in `clients//session-logs/` documenting what was resolved. Pull the ticket subject, comments, and resolution into a structured log. + +### Post to #bot-alerts (after every write) — MANDATORY + +After ANY successful write, post a one-line summary + a direct link to the affected item +into the Discord `#bot-alerts` channel. This is the team's live activity feed: every ticket +created / updated / closed / commented, every billing run, and every customer created shows +up there with who did it and a clickable link. It runs from a workstation or from the Discord +bot — the helper reads the bot token from the SOPS vault, so it works on any machine. + +**Scope — write operations only.** Pure reads (`/syncro` list, `ticket ` view, `search`, +`customers` search) create nothing and post no alert. Post only after the write call returns +success — never announce an action that did not complete. + +**Helper** (soft-fails if Discord is down; never blocks the workflow): + +```bash +bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" "" +``` + +**Message format:** `[SYNCRO] ` +- `` is the Syncro user from the Attribution table (the `identity.json` user running + the skill): `mike` → Mike, `howard` → Howard, etc. +- One line. Display the ticket **number** (`#32164`) in the text, but build the link from the + numeric **id** (`.ticket.id`), not the number. + +**Link by entity** (subdomain `computerguru.syncromsp.com`): + +| Entity affected | Link | +|---|---| +| Ticket — create / update / close / comment | `https://computerguru.syncromsp.com/tickets/` | +| Invoice — created during billing | `https://computerguru.syncromsp.com/invoices/` | +| Customer — created | `https://computerguru.syncromsp.com/customers/` | +| Comment / timer / line item on a ticket | link to the parent ticket | + +For a billing run that touches both a ticket and an invoice, send ONE alert: link the ticket +and state the invoice total (and prepay deduction if the customer is prepaid). + +**Examples:** + +```bash +# Ticket created +bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \ + "[SYNCRO] Howard created ticket #32301 (Desert Auto Tech — \"Server won't boot\") → https://computerguru.syncromsp.com/tickets/12350" + +# Ticket closed + billed (one alert) +bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \ + "[SYNCRO] Mike closed + invoiced #32164 (Jerry Burger) — 1.0h remote, \$150.00 → https://computerguru.syncromsp.com/tickets/12345" + +# Prepaid billing (zero-dollar invoice is correct — note the deduction) +bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \ + "[SYNCRO] Mike billed #32203 (Desert Auto Tech) — 1.5h onsite, applied 1.5 prepay hours, \$0.00 → https://computerguru.syncromsp.com/tickets/12300" + +# Customer created +bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \ + "[SYNCRO] Mike created customer \"Acme Plumbing\" → https://computerguru.syncromsp.com/customers/67890" +``` + +The helper escapes the message via `jq`, so quotes and `$` inside the text are safe. If the +bot token or network is unavailable it prints a `[WARNING]` and exits 0 — the alert is +best-effort and must never fail the Syncro write it follows. diff --git a/.claude/scripts/post-bot-alert.sh b/.claude/scripts/post-bot-alert.sh new file mode 100644 index 0000000..53a705f --- /dev/null +++ b/.claude/scripts/post-bot-alert.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# post-bot-alert.sh — post a one-line message to the Discord #bot-alerts channel. +# +# Usage: +# bash post-bot-alert.sh "message text" +# echo "message text" | bash post-bot-alert.sh +# +# Token resolution (first hit wins): +# 1. SOPS vault: projects/discord-bot/bot-token.sops.yaml field credentials.bot_token +# 2. projects/discord-bot/.env key DISCORD_TOKEN +# Reading from the vault means this works from any machine, not just BEAST. +# +# Soft-fail by design: if the message is empty, the token is missing, or Discord is +# unreachable, it prints a [WARNING] and exits 0 so it NEVER breaks the caller +# (e.g. the /syncro billing workflow). The alert is best-effort, not load-bearing. + +set -u + +CHANNEL_ID="624710699771232265" # #bot-alerts (Arizona Computer Guru guild) +ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" + +# --- message (arg or stdin) --- +MSG="${1:-}" +if [ -z "$MSG" ] && [ ! -t 0 ]; then MSG="$(cat)"; fi +if [ -z "$MSG" ]; then + echo "[WARNING] post-bot-alert: empty message — nothing sent" >&2 + exit 0 +fi + +# --- token: vault first, then .env --- +TOKEN="$(bash "$ROOT/.claude/scripts/vault.sh" get-field \ + projects/discord-bot/bot-token.sops.yaml credentials.bot_token 2>/dev/null)" +if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + ENV_FILE="$ROOT/projects/discord-bot/.env" + if [ -f "$ENV_FILE" ]; then + TOKEN="$(grep -iE '^[[:space:]]*DISCORD_TOKEN[[:space:]]*=' "$ENV_FILE" | head -1 \ + | sed -E 's/^[^=]*=[[:space:]]*//; s/^["'"'"']//; s/["'"'"'][[:space:]]*$//')" + fi +fi +if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + echo "[WARNING] post-bot-alert: no bot token (vault + .env both empty) — alert skipped" >&2 + exit 0 +fi + +# --- post (jq builds JSON so the message is safely escaped) --- +PAYLOAD="$(jq -nc --arg c "$MSG" '{content: $c}')" +RESP="$(curl -s -m 15 -w $'\n%{http_code}' \ + -X POST "https://discord.com/api/v10/channels/${CHANNEL_ID}/messages" \ + -H "Authorization: Bot ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "User-Agent: ClaudeToolsBot (claudetools, 1.0)" \ + --data-binary "$PAYLOAD" 2>/dev/null)" + +HTTP="$(printf '%s' "$RESP" | tail -n1)" +BODY="$(printf '%s' "$RESP" | sed '$d')" + +if [ "$HTTP" = "200" ]; then + MID="$(printf '%s' "$BODY" | jq -r '.id // empty' 2>/dev/null)" + echo "[OK] post-bot-alert: posted to #bot-alerts (message_id=${MID})" + exit 0 +fi + +echo "[WARNING] post-bot-alert: Discord returned ${HTTP:-no-response} — ${BODY}" >&2 +exit 0