John KuehJohn Kueh
All skills

Claude Code skill

daily-digest

Per-project morning roundup of signups, costs, and commits.

Install all skills in one command:

claude mcp add-plugin johnkueh/claude-skills

Why it exists

Each morning I want to know what actually moved across subs.rip, journeys.im and glp3.wiki — new signups, top user activities, LLM spend, recent commits. That data lives across BigQuery, Stripe, git and x-monitor logs. This skill stitches it all into one per-project digest so I don't have to open four dashboards before coffee.

In practice

Yesterday's roundup
Input
daily-digest subs.rip
Output
Signups: 47 (+12% wow). Top events: file_uploaded, srt_downloaded. LLM cost: $3.20. Top 3 commits. Latest x-monitor snapshot.
skills/daily-digest/SKILL.mdRaw
---
name: daily-digest
description: Per-project daily digest of new signups, top user activities, API/LLM costs, and recently-changed initiative docs — pulled from bq-analytics where available, plus git activity and the latest x-monitor run. Use when the user wants a "morning digest" across their projects, says "give me yesterday's roundup", or asks for a recurring summary across your projects. Designed to be invoked daily via `/loop`.
---

# daily-digest

Pulls a per-project digest covering, since the last check:

- **Users** — `total` distinct user_ids in `events.identifies` ever, plus `new` first-seen-in-window count (signups since last check)
- **Top user activities** — top events by count, with distinct-user fanout
- **API / LLM / scraping costs** — `cost.recorded` rolled up by provider/operation
- **Top error paths** — `logs.raw` 5xx count by path
- **Recent feedback** — `events.feedback` items

It also appends a per-handle **x-monitor section**. The script reads each subscribed handle's local archive (`tweets/<handle>.jsonl`) and filters to tweets within the digest window — it does **not** rely on the latest run file (that file would miss handles subscribed after the run was taken). No re-fetch — costs $0.

The Python script returns structured JSON; the agent (Claude) reformats it into a tight markdown digest.

## Setup

```bash
# from anywhere
uv run --quiet <skill-dir>/daily_digest.py setup
```

State lives at `~/.cache/daily-digest/` (`projects.json`, `state.json`, `runs/<iso>.json`). Override with `DAILY_DIGEST_HOME` if you want it on iCloud Drive for cross-machine sync.

## Add a project

```bash
uv run --quiet daily_digest.py add my-project \
    --bq-project my-project \
    --group-type household \
    --description "Cooklang recipe iOS app + Next.js + Hono API"
```

Flags:

| Flag | Required | Notes |
|---|---|---|
| `--bq-project` | required | GCP project that holds `events.raw`, `events.identifies`, `events.feedback`, `logs.raw` (the bq-analytics layout). |
| `--group-type` | optional | bq-analytics `group_type` to attach to each signup (e.g. `household`, `workspace`, `team`). When set, signups include the user's `group_id` and group name (`traits.display_name` or `traits.name`). |
| `--description` | optional | One-line note printed in the digest header. |
| `--force` | flag | Overwrite an existing project entry. |

`list`, `rm`, are also there:

```bash
uv run --quiet daily_digest.py list
uv run --quiet daily_digest.py rm my-saas
```

## Daily digest workflow (what `/loop` fires)

```
/loop 1d use the daily-digest skill to generate today's roundup
```

When invoked:

1. **Run the check.** Lookback window = `now − last_checked_at` per project (default 1 day on first run). Pass `--since 24h` to override, or `--no-state` to leave `last_checked_at` untouched (useful for ad-hoc runs).

   ```bash
   uv run --quiet daily_digest.py check --json
   ```

2. **Reformat the JSON into a tight bulleted digest.** Use proper markdown — `#` title, `##` per project / x-monitor, real bullet lists. Structure (NOT style) stays minimal: just signups, events, costs, optional 5xx/feedback, then x-monitor. No priorities, no initiatives, no takeaway.

   ````md
   # Daily digest — DD/MM/YYYY

   ## <project>

   - **Users:** new N, total M _(group: Household Name, …)_   ← group only if new>0 and --group-type set
   - **Events:** <rough total> — `event_a` N×, `event_b` N×, `event_c` N×
   - **Costs:** $X.XX total — `provider/op` $X.XX, `provider/op` $X.XX, …
   - **5xx:** `/path` N×, …                           ← omit line entirely when zero
   - **Feedback:** N items — _[kind] subject_         ← omit line entirely when zero

   ## <next project>
   …

   ## X-monitor

   - No new posts from @a or @b
     OR
   - **@a** — N× new: one-line summary
   ````

   Rules:
   - **Date format `DD/MM/YYYY`** in the title.
   - **`##` for each project** and for x-monitor — gives clear visual separation when scanning multiple projects.
   - **Bold the field name** (`**Signups:**`, `**Events:**`, …); inline-code the event names / paths / providers.
   - **Round large counts** — 1865 → `1.9k`, 12340 → `12k+`. Exact under 1000.
   - **Top 3 events / top 4–5 cost ops** inline, comma-separated. No nested bullets.
   - **Omit 5xx and feedback bullets when zero** — always show signups, events, costs.
   - **No priorities, no initiatives, no takeaway, no emojis.** This is a glance, not a briefing.

3. **Append the x-monitor section** from `digest.x_monitor` — the script already filtered each subscribed handle's archive to the digest window; just summarize per-handle new tweets following the x-monitor skill's bullet format.

4. **One-line takeaway across projects** — what stands out today. Skip if nothing did.

5. **State advances automatically.** The next `check` only covers what's happened since this run. Pass `--no-state` to skip the advance for ad-hoc questions ("what's happened in the last 7 days?").

## Smoke test (one-shot, no state)

```bash
uv run --quiet daily_digest.py check --since 24h --no-state --json | jq
```

This runs all configured projects with a fixed 24h window and **doesn't** advance `last_checked_at`. Use for development.

## What the JSON looks like

```jsonc
{
  "generated_at": "2026-05-07T06:10:27Z",
  "projects": [
    {
      "name": "my-project",
      "since": "2026-05-06T06:10:29Z",
      "description": "...",
      "signups":   { "new_users": 1, "sample": [...] },
      "activities":{ "top": [{event_name, n, users}, ...] },
      "costs":     { "total_usd": 0.94, "total_calls": 179, "breakdown": [...] },
      "errors":    { "top": [{path, errs, total}, ...] },
      "feedback":  { "items": [{ts, kind, subject, message, user_id}, ...] },
      "initiatives": {
         "dir": "/abs/path/to/docs/initiatives",
         "total_files": 45,
         "readme": "...full markdown content of README.md...",
         "files": [
           { "file": "...", "title": "...", "summary": "...",
             "mtime": "...", "size_bytes": N,
             "modified_recently": true }, ...
         ]
      }
    }
  ],
  "x_monitor": { ...latest x-monitor run JSON... }
}
```

Per-project fields are omitted when their source isn't configured (e.g. no `bq_project` → no `signups`/`activities`/`costs`/`errors`/`feedback`).

## Schema assumptions

The bq fetchers assume the `bq-analytics` layout:

- `events.raw` — `event_name`, `user_id`, `ts`, JSON `properties`. Cost events use `event_name = 'cost.recorded'` with `properties.{provider, operation, model, cost_micros}`.
- `events.identifies` — `user_id`, `ts`, JSON `traits`. First-seen-per-`user_id` = signup.
- `events.feedback` — `kind`, `subject`, `message`, `user_id`, `ts`.
- `logs.raw` — `ts`, `level`, `path`, `status`, `message` (Vercel Log Drain landing zone).

If your project doesn't follow this exact shape, edit `bq_signups`/`bq_costs`/etc. in `daily_digest.py` — they're independent ~15-line functions.

## Costs

- bq queries scan a few MB per project → free under the 1 TB/mo BQ on-demand quota.
- x-monitor section reads the **already-archived** latest run, so $0 in API calls. (The actual fresh fetch happens in the x-monitor skill's own `/loop`.)
- Total cost of a daily digest across 5 projects with bq-analytics: $0.

## Troubleshooting

- **`Access Denied: User does not have bigquery.jobs.create permission`** — switch gcloud accounts: `gcloud config set account <email>`. The skill calls `bq` via subprocess, so it inherits whatever account is active.
- **`signups.new_users = 0` but you know there were sign-ups** — verify `events.identifies` is being written; some projects only call `track()` and never `identify()`. In that case, treat first-seen-in-`events.raw` as the signup proxy and add a fetcher.
- **No files flagged `modified_recently`** — mtime floor is 7 days. If recent edits don't appear, the files might have been touched only in git history, not on the local filesystem (e.g. checkout from a different branch). Run `touch` or widen `INITIATIVES_LOOKBACK`.
- **x-monitor section says "no runs yet"** — set `X_MONITOR_HOME` to point at the synced folder, or run x-monitor's `setup` + `check` once.

## Recurring with /loop

```
/loop 1d use the daily-digest skill to produce a digest across configured projects
```

For a self-paced cadence (let the model decide when):

```
/loop use the daily-digest skill whenever appropriate to summarize project activity
```