← All Skills

snappy-inbox-sweep

v1.0.0
84 files, 705.6 KB ~2,475 words · 10 min read Updated 2026-09-09

snappy-inbox-sweep skill

41 of 47 checks pass
What it can do
gmail-personalread
gmail-workread
inbox-zerowrite
send-skool post-url textpost
send-slack channel textsend
slackread
sweepwrite
What does not pass yet
$ npx snappy-skills install snappy-inbox-sweep
zip ↓
File Tree
├── AGENTS.md ├── SKILL.md ├── api.ts ├── contract.test.ts ├── doors.test.ts ├── entities.json ├── gmail-oauth.ts ├── label-audit.ts ├── label-clean.ts ├── label-delete.ts ├── label-ids.ts ├── label-nuke.ts ├── label-sample.ts ├── linkedin-fetcher.ts ├── linkedin-sweep-cron.sh ├── out/ │ ├── 2026-04-09T20-46-inbox.json │ ├── 2026-04-10T01-25-inbox.json │ ├── 2026-04-13T13-41-inbox.json │ ├── 2026-04-13T13-42-inbox.json │ ├── 2026-04-13T13-49-inbox.json │ ├── 2026-04-13T13-52-inbox-zero.json │ ├── 2026-04-13T13-52-inbox.json │ ├── 2026-04-13T14-01-inbox-zero.json │ ├── 2026-04-13T14-01-inbox.json │ ├── 2026-04-13T14-02-inbox-zero.json │ ├── 2026-04-13T14-02-inbox.json │ ├── 2026-04-13T14-03-inbox-zero.json │ ├── 2026-04-13T14-03-inbox.json │ ├── 2026-04-13T14-04-inbox-zero.json │ ├── 2026-04-13T14-04-inbox.json │ ├── 2026-04-13T14-05-inbox-zero.json │ ├── 2026-04-13T14-05-inbox.json │ ├── 2026-04-13T14-07-inbox-zero.json │ ├── 2026-04-13T14-07-inbox.json │ ├── 2026-04-13T14-08-inbox-zero.json │ ├── 2026-04-13T14-08-inbox.json │ ├── 2026-04-13T14-09-inbox-zero.json │ ├── 2026-04-13T14-09-inbox.json │ ├── 2026-04-13T14-10-inbox-zero.json │ ├── 2026-04-13T14-10-inbox.json │ ├── 2026-04-13T14-11-inbox-zero.json │ ├── 2026-04-13T14-11-inbox.json │ ├── 2026-04-13T14-12-inbox-zero.json │ ├── 2026-04-13T14-12-inbox.json │ ├── 2026-04-13T14-13-inbox-zero.json │ ├── 2026-04-13T14-13-inbox.json │ ├── 2026-04-13T14-14-inbox-zero.json │ ├── 2026-04-13T14-14-inbox.json │ ├── 2026-04-13T14-39-inbox-zero.json │ ├── 2026-04-13T14-39-inbox.json │ ├── 2026-04-13T14-40-inbox-zero.json │ ├── 2026-04-13T14-40-inbox.json │ ├── 2026-04-13T17-12-inbox-zero.json │ ├── 2026-04-13T17-12-inbox.json │ ├── 2026-04-13T17-13-inbox.json │ ├── 2026-04-13T17-23-inbox.json │ ├── 2026-04-13T17-24-inbox.json │ ├── 2026-04-13T18-35-inbox-zero.json │ ├── 2026-04-13T18-35-inbox.json │ ├── 2026-04-13T18-36-inbox.json │ ├── 2026-04-13T18-40-inbox-zero.json │ ├── 2026-04-13T18-40-inbox.json │ ├── 2026-04-13T19-32-inbox.json │ ├── 2026-04-13T19-38-inbox-zero.json │ ├── 2026-04-13T19-38-inbox.json │ ├── 2026-04-13T20-40-inbox.json │ ├── 2026-04-13T20-41-inbox-zero.json │ ├── 2026-04-13T20-49-inbox-zero.json │ ├── 2026-04-13T20-49-inbox.json │ ├── 2026-04-13T20-51-inbox-zero.json │ ├── 2026-04-13T20-51-inbox.json │ ├── 2026-04-13T21-01-inbox-zero.json │ ├── 2026-04-13T21-01-inbox.json │ ├── 2026-04-13T21-02-inbox-zero.json │ ├── 2026-04-13T21-02-inbox.json │ ├── 2026-04-14T05-07-inbox.json │ ├── 2026-04-15T04-29-inbox.json │ ├── 2026-04-15T04-54-inbox.json │ ├── 2026-04-15T04-59-inbox.json │ ├── 2026-04-15T14-32-inbox.json │ ├── 2026-04-15T16-41-inbox.json │ ├── 2026-04-15T16-46-inbox.json │ └── 2026-04-15T16-49-inbox.json └── skool-fetcher.ts
Documents
AGENTS.md

snappy-inbox-sweep — Agent Loader#

Purpose#

Compress the "log into Slack + Gmail + LinkedIn + Skool + StateChange to check messages" routine into one command. Fetch is deterministic (scripts). Agent judgment only at the reply-drafting step.

Channel status#

Channel Fetch Archive/Mark-read Send Notes
Slack ready ready (conversations.mark) ready SLACK_USER_TOKEN in .env.cache; the reply arm spawns snappy-slack, so SLACK_BOT_TOKEN too
Gmail (work) ready ready (gmail.modify) send via snappy-email Service account JWT for robert@snappy.ai
Gmail (personal) ready ready (gmail.modify) OAuth refresh token re-consented 2026-04-13 with gmail.modify scope
LinkedIn DMs ready (fetch) blocked ready (browser) linkedin-fetcher.ts; send via sendLinkedInDM() (headed browser)
Skool ready (fetch) blocked ready (browser) browser scrape via snappy-browse; skool-fetcher.ts; reply via sendSkoolReply()
WhatsApp intentionally empty CLAUDE.md says skip

Active: Slack + Gmail (work + personal, full archive) + LinkedIn DMs (fetch + send) + Skool (fetch + reply). Archive not yet available for browser channels.

Protocol#

Inbox Zero (primary — use this)#

  1. Run inboxZero() — fetch all channels, classify noise vs actionable, archive noise automatically, return actionable only.
  2. Draft replies for actionable items per Robert's tone rules (see below).
  3. Present numbered inline in chat: [N] source · user · preview → draft.
  4. Wait for approval. Never auto-send.
  5. Send via per-channel function after Robert confirms.
  6. Mark Slack as read for channels where all items were handled.
certificate:
  premises:   all channel fetchers return successfully; noise patterns match known junk senders
  action:     inboxZero() → classify → batchArchiveGmail() → markSlackRead()
  trace:      noise items removed from inbox; re-fetch should show reduced unread count
  evidence:   independent re-fetch of each Gmail account + Slack after archive completes
  conclusion: PASS if re-fetch count drops by archived count. FAIL if counts unchanged.

Sweep only (read-only, no archiving)#

  1. Run sweepAll() — parallel fetch across ready channels. Writes out/<stamp>-inbox.json.
  2. Read items, draft replies per Robert's tone rules (see below). Hard-skip anything you can't draft substantively — mark as like_only or skip, never post empty thanks.
  3. Present numbered inline in chat: [N] source · user · preview → draft.
  4. Wait for approval. Never auto-send.
  5. Send via per-channel function after Robert confirms.

Tone rules (non-negotiable, from CLAUDE.md)#

  • No em/en dashes. Use commas/periods.
  • No obvious encouragement, no stock phrases ("appreciate that", "game changer", "the feeling is mutual").
  • No empty thanks. If nothing substantive, mark like_only.
  • Never start threaded replies with the person's name (UI shows it).
  • Replies must have information — teach, answer, or add context.
  • Don't try to be clever. Respond directly.

API module#

typescriptimport {
  inboxZero, sweepAll, classifyAll,
  fetchSlackUnread, fetchGmailUnread,
  archiveGmail, batchArchiveGmail, markSlackRead,
  sendSlackReply, sendSkoolReply,
  InboxItem, ClassifiedItem, InboxZeroResult,
} from "../snappy-inbox-sweep/api.ts";
Function Purpose
inboxZero() Full inbox zero: fetch → classify → archive noise → mark Slack read → return actionable
sweepAll() Read-only sweep: fetch all channels, write out/<stamp>-inbox.json, return {items, errors, file}
classifyAll(items) Classify items as noise vs actionable based on pattern matching
fetchSlackUnread() Return InboxItem[] for all Slack DMs with unread_count > 0
fetchGmailUnread(account) Return InboxItem[] for Gmail account ("robert@snappy.ai" or "robertjboulos@gmail.com")
fetchLinkedInDMs() Return InboxItem[] for unread LinkedIn DMs (browser scrape via linkedin-fetcher.ts)
archiveGmail(account, messageId) Archive a single Gmail message (remove INBOX + UNREAD labels)
batchArchiveGmail(account, messageIds) Archive multiple Gmail messages, return success count
markSlackRead(channel, ts) Mark Slack conversation as read up to timestamp
sendSlackReply(channel, text, thread_ts?) Post approved reply to Slack
sendLinkedInDM(conversationUrl, message) Send a DM reply in a LinkedIn conversation via headed browser (linkedin-fetcher.ts)
sendSkoolReply(postUrl, message) Post a reply/comment on a Skool post via browser automation (headed mode, returns {success, posted?, error?})

CLI:

bashcd ~/.claude/skills/snappy-inbox-sweep
npx tsx api.ts inbox-zero     # full inbox zero: archive noise, return actionable
npx tsx api.ts sweep          # read-only sweep, writes out/ + prints JSON
npx tsx api.ts slack          # just Slack
npx tsx api.ts gmail-work     # just Gmail work
npx tsx api.ts gmail-personal # just Gmail personal
npx tsx linkedin-fetcher.ts   # just LinkedIn DMs (browser scrape)
npx tsx linkedin-fetcher.ts send <conversation-url> "reply text"  # send LinkedIn DM
npx tsx api.ts send-slack D12345 "reply text"
npx tsx api.ts send-skool "https://www.skool.com/snappy/post-slug" "reply text"
npx tsx skool-fetcher.ts reply "https://www.skool.com/snappy/post-slug" "reply text"

Adding a new channel (graduation path)#

  1. Add a fetcher to api.ts that returns InboxItem[] (use the shape). Direct API call or playwright via snappy-browse.
  2. Add it to the Promise.all in sweepAll().
  3. Manually run npx tsx api.ts sweep 10+ times. Verify output quality. Do NOT cron.
  4. Add a corresponding send* function.
  5. Update the channel table in this file + SKILL.md.
  6. Log graduation in ~/.claude/logs/agents-md-feedback.log.

Uses#

Skill Why
snappy-settings env() loader
snappy-slack has its own sendDm — use it if you prefer its wrapper
snappy-email send side when Gmail fetcher exists
snappy-browse playwright runtime for browser channels
snappy-inbound future: skip items already handled by inbound bot

Used by#

Nothing in the collection names this skill.

<!-- SNAPPY-CONTRACT-VERBS-START -->

Contract verbs#

Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.

Verb Contract arguments Effect First call
gmail-personal read npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts gmail-personal
gmail-work read npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts gmail-work
inbox-zero write npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts inbox-zero
send-skool post-url, text post npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts send-skool <post-url> "<text>"
send-slack channel, text send npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts send-slack <channel> "<text>"
slack read npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts slack
sweep write npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts sweep

Show the result#

When an answer carries face_hint, show it with one snappy_present(<answer>) call.

See /snappy-faces for face selection. Human-facing images must crop to the

element, render at 2x on Retina, and fill the destination channel instead of

placing a small card in a full-page screenshot.

<!-- SNAPPY-CONTRACT-VERBS-END -->


Self-correcting loader (PID feedback)#

The system spec lives at ~/.claude/skills/snappy-settings/skill-spec.md. It defines the entire Snappy operating system. You enforce it.

Before you finish, do two things:#

1. Fix gaps (P — proportional). MANDATORY. If this AGENTS.md didn't cover your case — if you had to read another file, run a command not documented here, work around a wrong selector, or discover a quirk — you MUST attempt an Edit before you can log.

  • Default action: edit this AGENTS.md. Surgical. Add the missing line, table row, or code example where it belongs. No restructuring.
  • Only LOGGED is allowed when: the fix needs >10 lines, spans multiple files, or requires a structural rewrite (in which case the drain-skill-regen.sh queue picks it up).
  • The goal: the next agent never has to leave the loader.

2. Validate structure. Check this skill against the spec:

  • api.ts exists with typed exports, CLI mode, import { env } from "../snappy-settings/load.ts"
  • api.ts calls APIs directly (no Xano proxy, unless this skill IS a Xano DB skill)
  • AGENTS.md has API module section with import + function table + CLI
  • No shell-command fallbacks, no hardcoded tokens anywhere
  • If any check fails → fix it or log it

3. Log always.

bashecho "[$(date -u +%FT%TZ)] snappy-inbox-sweep: <what was missing or fixed> [FIXED|LOGGED]" >> ~/.claude/logs/agents-md-feedback.log

FIXED = you patched it. LOGGED = too large for inline fix, queued for drain-skill-regen.sh.

Do not skip this. Every agent run must leave the system better than it found it.

Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.

<!-- SKILL-INDEX-START -->

[snappy-inbox-sweep Index]|root: ~/.claude/skills/snappy-inbox-sweep|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}

<!-- SKILL-INDEX-END -->

---
name: snappy-inbox-sweep
role: Deterministic multi-channel inbox sweep. Fetches unread via scripts (not agents), drafts replies in Robert's tone, presents for approval inline, sends on confirm.
loaded-by: UserPromptSubmit + PreToolUse hook (auto-injected when "snappy-inbox-sweep" or related keywords are mentioned)
---

# snappy-inbox-sweep — Agent Loader

## Purpose

Compress the "log into Slack + Gmail + LinkedIn + Skool + StateChange to check messages" routine into one command. Fetch is deterministic (scripts). Agent judgment only at the reply-drafting step.

## Channel status

| Channel | Fetch | Archive/Mark-read | Send | Notes |
|---|---|---|---|---|
| Slack | ready | ready (conversations.mark) | ready | `SLACK_USER_TOKEN` in .env.cache; the reply arm spawns `snappy-slack`, so `SLACK_BOT_TOKEN` too |
| Gmail (work) | ready | ready (gmail.modify) | send via snappy-email | Service account JWT for robert@snappy.ai |
| Gmail (personal) | ready | ready (gmail.modify) | — | OAuth refresh token re-consented 2026-04-13 with gmail.modify scope |
| LinkedIn DMs | ready (fetch) | blocked | ready (browser) | `linkedin-fetcher.ts`; send via `sendLinkedInDM()` (headed browser) |
| Skool | ready (fetch) | blocked | ready (browser) | browser scrape via snappy-browse; `skool-fetcher.ts`; reply via `sendSkoolReply()` |
| WhatsApp | intentionally empty | — | — | CLAUDE.md says skip |

**Active: Slack + Gmail (work + personal, full archive) + LinkedIn DMs (fetch + send) + Skool (fetch + reply).** Archive not yet available for browser channels.

## Protocol

### Inbox Zero (primary — use this)

1. **Run `inboxZero()`** — fetch all channels, classify noise vs actionable, archive noise automatically, return actionable only.
2. **Draft replies** for actionable items per Robert's tone rules (see below).
3. **Present numbered inline** in chat: `[N] source · user · preview → draft`.
4. **Wait for approval.** Never auto-send.
5. **Send via per-channel function** after Robert confirms.
6. **Mark Slack as read** for channels where all items were handled.

```
certificate:
  premises:   all channel fetchers return successfully; noise patterns match known junk senders
  action:     inboxZero() → classify → batchArchiveGmail() → markSlackRead()
  trace:      noise items removed from inbox; re-fetch should show reduced unread count
  evidence:   independent re-fetch of each Gmail account + Slack after archive completes
  conclusion: PASS if re-fetch count drops by archived count. FAIL if counts unchanged.
```

### Sweep only (read-only, no archiving)

1. **Run `sweepAll()`** — parallel fetch across ready channels. Writes `out/<stamp>-inbox.json`.
2. **Read items, draft replies** per Robert's tone rules (see below). Hard-skip anything you can't draft substantively — mark as `like_only` or `skip`, never post empty thanks.
3. **Present numbered inline** in chat: `[N] source · user · preview → draft`.
4. **Wait for approval.** Never auto-send.
5. **Send via per-channel function** after Robert confirms.

## Tone rules (non-negotiable, from CLAUDE.md)

- No em/en dashes. Use commas/periods.
- No obvious encouragement, no stock phrases ("appreciate that", "game changer", "the feeling is mutual").
- No empty thanks. If nothing substantive, mark `like_only`.
- Never start threaded replies with the person's name (UI shows it).
- Replies must have information — teach, answer, or add context.
- Don't try to be clever. Respond directly.

## API module

```typescript
import {
  inboxZero, sweepAll, classifyAll,
  fetchSlackUnread, fetchGmailUnread,
  archiveGmail, batchArchiveGmail, markSlackRead,
  sendSlackReply, sendSkoolReply,
  InboxItem, ClassifiedItem, InboxZeroResult,
} from "../snappy-inbox-sweep/api.ts";
```

| Function | Purpose |
|---|---|
| `inboxZero()` | Full inbox zero: fetch → classify → archive noise → mark Slack read → return actionable |
| `sweepAll()` | Read-only sweep: fetch all channels, write `out/<stamp>-inbox.json`, return `{items, errors, file}` |
| `classifyAll(items)` | Classify items as noise vs actionable based on pattern matching |
| `fetchSlackUnread()` | Return `InboxItem[]` for all Slack DMs with unread_count > 0 |
| `fetchGmailUnread(account)` | Return `InboxItem[]` for Gmail account (`"robert@snappy.ai"` or `"robertjboulos@gmail.com"`) |
| `fetchLinkedInDMs()` | Return `InboxItem[]` for unread LinkedIn DMs (browser scrape via `linkedin-fetcher.ts`) |
| `archiveGmail(account, messageId)` | Archive a single Gmail message (remove INBOX + UNREAD labels) |
| `batchArchiveGmail(account, messageIds)` | Archive multiple Gmail messages, return success count |
| `markSlackRead(channel, ts)` | Mark Slack conversation as read up to timestamp |
| `sendSlackReply(channel, text, thread_ts?)` | Post approved reply to Slack |
| `sendLinkedInDM(conversationUrl, message)` | Send a DM reply in a LinkedIn conversation via headed browser (`linkedin-fetcher.ts`) |
| `sendSkoolReply(postUrl, message)` | Post a reply/comment on a Skool post via browser automation (headed mode, returns `{success, posted?, error?}`) |

CLI:

```bash
cd ~/.claude/skills/snappy-inbox-sweep
npx tsx api.ts inbox-zero     # full inbox zero: archive noise, return actionable
npx tsx api.ts sweep          # read-only sweep, writes out/ + prints JSON
npx tsx api.ts slack          # just Slack
npx tsx api.ts gmail-work     # just Gmail work
npx tsx api.ts gmail-personal # just Gmail personal
npx tsx linkedin-fetcher.ts   # just LinkedIn DMs (browser scrape)
npx tsx linkedin-fetcher.ts send <conversation-url> "reply text"  # send LinkedIn DM
npx tsx api.ts send-slack D12345 "reply text"
npx tsx api.ts send-skool "https://www.skool.com/snappy/post-slug" "reply text"
npx tsx skool-fetcher.ts reply "https://www.skool.com/snappy/post-slug" "reply text"
```

## Adding a new channel (graduation path)

1. Add a fetcher to `api.ts` that returns `InboxItem[]` (use the shape). Direct API call or playwright via snappy-browse.
2. Add it to the `Promise.all` in `sweepAll()`.
3. Manually run `npx tsx api.ts sweep` 10+ times. Verify output quality. Do NOT cron.
4. Add a corresponding `send*` function.
5. Update the channel table in this file + SKILL.md.
6. Log graduation in `~/.claude/logs/agents-md-feedback.log`.

## Uses

| Skill | Why |
|---|---|
| `snappy-settings` | env() loader |
| `snappy-slack` | has its own `sendDm` — use it if you prefer its wrapper |
| `snappy-email` | send side when Gmail fetcher exists |
| `snappy-browse` | playwright runtime for browser channels |
| `snappy-inbound` | future: skip items already handled by inbound bot |

## Used by

Nothing in the collection names this skill.

<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs

Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.

| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `gmail-personal` | — | `read` | `npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts gmail-personal` |
| `gmail-work` | — | `read` | `npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts gmail-work` |
| `inbox-zero` | — | `write` | `npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts inbox-zero` |
| `send-skool` | `post-url`, `text` | `post` | `npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts send-skool <post-url> "<text>"` |
| `send-slack` | `channel`, `text` | `send` | `npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts send-slack <channel> "<text>"` |
| `slack` | — | `read` | `npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts slack` |
| `sweep` | — | `write` | `npx tsx ~/.claude/skills/snappy-inbox-sweep/api.ts sweep` |

## Show the result

When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->

---

## Self-correcting loader (PID feedback)

The system spec lives at `~/.claude/skills/snappy-settings/skill-spec.md`. It defines the entire Snappy operating system. You enforce it.

### Before you finish, do two things:

**1. Fix gaps (P — proportional). MANDATORY.** If this AGENTS.md didn't cover your case — if you had to read another file, run a command not documented here, work around a wrong selector, or discover a quirk — you MUST attempt an `Edit` before you can log.

- **Default action: edit this AGENTS.md.** Surgical. Add the missing line, table row, or code example where it belongs. No restructuring.
- **Only LOGGED is allowed when:** the fix needs >10 lines, spans multiple files, or requires a structural rewrite (in which case the drain-skill-regen.sh queue picks it up).
- The goal: the next agent never has to leave the loader.

**2. Validate structure.** Check this skill against the spec:
- `api.ts` exists with typed exports, CLI mode, `import { env } from "../snappy-settings/load.ts"`
- `api.ts` calls APIs directly (no Xano proxy, unless this skill IS a Xano DB skill)
- `AGENTS.md` has API module section with import + function table + CLI
- No shell-command fallbacks, no hardcoded tokens anywhere
- If any check fails → fix it or log it

**3. Log always.**
```bash
echo "[$(date -u +%FT%TZ)] snappy-inbox-sweep: <what was missing or fixed> [FIXED|LOGGED]" >> ~/.claude/logs/agents-md-feedback.log
```
`FIXED` = you patched it. `LOGGED` = too large for inline fix, queued for drain-skill-regen.sh.

**Do not skip this.** Every agent run must leave the system better than it found it.


Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.

<!-- SKILL-INDEX-START -->
[snappy-inbox-sweep Index]|root: ~/.claude/skills/snappy-inbox-sweep|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
<!-- SKILL-INDEX-END -->

Keyboard Shortcuts

Search in document⌘K
Focus search/
Previous file tab
Next file tab
Close overlayEsc
Show shortcuts?