Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions __tests__/inbox-watch-plugin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const fs = require("fs")
const path = require("path")
const { runNoServer, makePluginEnv, withInstalledPlugin } = require("./helpers/plugin-test-env")

// The real tool follows cli-output-spec: versioned JSON on stdout, typed errors
// on stderr, and a semantic exit code -- 10 meaning "new items", which is a
// SUCCESS signal a cron acts on, not a failure. The fake reproduces that shape.
function writeFakeInboxWatchBinary(dir) {
const bin = path.join(dir, "inbox-watch")
fs.writeFileSync(bin, [
"#!/usr/bin/env node",
"const args = process.argv.slice(2);",
"if (args[0] === 'help-json') {",
" console.log(JSON.stringify({ version: '1.0.0', output: 'json', interactive: false,",
" commands: { 'help-json': { args: [] } },",
" exit_codes: { '0': 'success — nothing new', '10': 'success — new items found (the cron signal)' },",
" env: ['INBOX_TOKEN'] })); process.exit(0);",
"}",
"if (args[0] === 'guide') {",
" console.log(JSON.stringify({ version: '1.0.0', summary: 'tells you only when a human replied',",
" gotchas: ['The cursor is consumed on read: use --peek to diagnose.'] })); process.exit(0);",
"}",
"// --peek reports WITHOUT consuming; a bare run consumes and exits 10 when new.",
"const peek = args.includes('--peek');",
"const exitZero = args.includes('--exit-zero');",
"const item = { channel: 'mailbox', kind: 'reply', from: 'someone@example.com', title: 'Re: hello', url: '' };",
"console.log(JSON.stringify({ ok: true, version: '1.0.0', new: [item], count: 1,",
" disabled: [{ channel: 'resend', reason: 'set RESEND_API_KEY', recoverable: true }] }));",
"process.exit(exitZero ? 0 : 10);"
].join("\n"), "utf-8")
fs.chmodSync(bin, 0o755)
return bin
}

describe("inbox-watch plugin", () => {
const ctx = makePluginEnv("inbox-watch")
writeFakeInboxWatchBinary(ctx.fakeDir)
withInstalledPlugin(ctx)

test("exposes the cli-output-spec command catalog", () => {
const r = runNoServer("inbox-watch self help-json --json", { env: ctx.env })
expect(r.ok).toBe(true)
const data = JSON.parse(r.output)
expect(data.command).toBe("inbox-watch.self.help-json")
expect(data.data.version).toBe("1.0.0")
// exit code 10 must be documented as a success signal, not an error
expect(data.data.exit_codes["10"]).toMatch(/success/)
})

test("routes the embedded guide", () => {
const r = runNoServer("inbox-watch self guide --json", { env: ctx.env })
expect(r.ok).toBe(true)
const data = JSON.parse(r.output)
expect(data.command).toBe("inbox-watch.self.guide")
expect(data.data.gotchas.join(" ")).toMatch(/--peek/)
})

test("returns the versioned envelope with classified items", () => {
const r = runNoServer("inbox-watch inbox check --json", { env: ctx.env })
expect(r.ok).toBe(true)
const data = JSON.parse(r.output)
expect(data.command).toBe("inbox-watch.inbox.check")
expect(data.data.ok).toBe(true)
expect(data.data.new[0].kind).toBe("reply")
// a disabled channel is structured context, not prose
expect(data.data.disabled[0]).toHaveProperty("recoverable")
})

test("exit 10 (new items) is a SUCCESS, not a failure", () => {
// supercli's process adapter treats any non-zero exit as failure, and
// inbox-watch exits 10 when mail arrived -- so routed naively, the single
// most important case would surface as an error. The plugin normalises it.
const r = runNoServer("inbox-watch inbox check --json", { env: ctx.env })
expect(r.ok).toBe(true)
expect(JSON.parse(r.output).data.count).toBe(1)
})

test("peek is a distinct command from check", () => {
// They are separate on purpose: the destructive one must never be the one
// reached for while debugging, since a normal run consumes the cursor and
// eats the alert the cron would have sent.
const r = runNoServer("inbox-watch inbox peek --json", { env: ctx.env })
expect(r.ok).toBe(true)
expect(JSON.parse(r.output).command).toBe("inbox-watch.inbox.peek")
})
Comment on lines +78 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Assert that --peek reaches the binary.

This only checks the wrapper command name. The fake already calculates peek at Line 24, but never returns it, so removing --peek from the manifest still passes. Include peek in the fake JSON and assert data.data.peek === true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/inbox-watch-plugin.test.js` around lines 78 - 85, Update the fake
command response used by the “peek is a distinct command from check” test to
include its calculated peek value, then parse the JSON output and assert
data.data.peek is true so the test verifies --peek reaches the binary rather
than only checking the wrapper command name.

})
12 changes: 12 additions & 0 deletions plugins/inbox-watch/install-guidance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"plugin": "inbox-watch",
"binary": "inbox-watch",
"check": "which inbox-watch",
"install_steps": [
"git clone https://github.com/javimosch/inbox-watch /tmp/inbox-watch",
"install -m755 /tmp/inbox-watch/inbox-watch ~/.local/bin/inbox-watch",
"mkdir -p ~/.inbox-watch && cp /tmp/inbox-watch/config.example.json ~/.inbox-watch/config.json",
"Verify: inbox-watch help-json"
],
Comment on lines +5 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add the configuration step to the standalone flow.

After Line 8, instruct users to edit ~/.inbox-watch/config.json before verification; the inline guidance does this, but this standalone path does not. Otherwise a successful install can monitor nothing.

Proposed fix
     "mkdir -p ~/.inbox-watch && cp /tmp/inbox-watch/config.example.json ~/.inbox-watch/config.json",
+    "Edit ~/.inbox-watch/config.json (what to watch); secrets go in env vars",
     "Verify: inbox-watch help-json"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"install_steps": [
"git clone https://github.com/javimosch/inbox-watch /tmp/inbox-watch",
"install -m755 /tmp/inbox-watch/inbox-watch ~/.local/bin/inbox-watch",
"mkdir -p ~/.inbox-watch && cp /tmp/inbox-watch/config.example.json ~/.inbox-watch/config.json",
"Verify: inbox-watch help-json"
],
"install_steps": [
"git clone https://github.com/javimosch/inbox-watch /tmp/inbox-watch",
"install -m755 /tmp/inbox-watch/inbox-watch ~/.local/bin/inbox-watch",
"mkdir -p ~/.inbox-watch && cp /tmp/inbox-watch/config.example.json ~/.inbox-watch/config.json",
"Edit ~/.inbox-watch/config.json (what to watch); secrets go in env vars",
"Verify: inbox-watch help-json"
],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/inbox-watch/install-guidance.json` around lines 5 - 10, Update the
install_steps sequence in install-guidance.json to add an instruction after
copying config.example.json that tells users to edit ~/.inbox-watch/config.json
and configure monitoring targets before running the existing verification step.

"note": "Single Python 3 file, no dependencies. No auth required by the tool itself; each channel needs its own secret in an env var."
}
5 changes: 5 additions & 0 deletions plugins/inbox-watch/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "Poll GitHub notifications, IMAP and Resend inbound for items new since the last run. Classifies DMARC reports, out-of-office autoreplies and real human replies so alerts stay worth reading. Exit 10 when something arrived — cron-shaped. Agent-first: versioned JSON on stdout, typed errors on stderr, semantic exit codes, help-json and guide.",
"tags": ["inbox-watch", "cli", "email", "notifications", "monitoring", "agent-first", "cron"],
"has_learn": true
}
124 changes: 124 additions & 0 deletions plugins/inbox-watch/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
{
"name": "inbox-watch",
"version": "1.0.0",
"description": "Tell me only when a human actually replied \u2014 poll GitHub/IMAP/Resend inbound, classify DMARC vs autoreply vs reply, exit 10 when something real arrived",
"source": "https://github.com/javimosch/inbox-watch",
"checks": [
{
"type": "binary",
"name": "inbox-watch"
}
],
"install_guidance": {
"plugin": "inbox-watch",
"binary": "inbox-watch",
"check": "which inbox-watch",
"install_steps": [
"git clone https://github.com/javimosch/inbox-watch /tmp/inbox-watch",
"install -m755 /tmp/inbox-watch/inbox-watch ~/.local/bin/inbox-watch",
"mkdir -p ~/.inbox-watch && cp /tmp/inbox-watch/config.example.json ~/.inbox-watch/config.json",
"Edit ~/.inbox-watch/config.json (what to watch); secrets go in env vars",
"Verify: inbox-watch help-json",
"supercli plugins install ./plugins/inbox-watch --on-conflict replace --json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file context =="
sed -n '1,140p' plugins/inbox-watch/plugin.json

echo
echo "== occurrences of native install command syntax =="
rg -n "plugins install|supercli plugins install|sc plugins install|SuperCLI|native" -S plugins README.md . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200

echo
echo "== sc availability =="
command -v sc || true
command -v sc-zig || true
if command -v sc >/tmp/sc_bin 2>/tmp/sc_err; then
  bin="$(cat /tmp/sc_bin)"
  echo "SC_BIN=$bin"
  sc --version 2>/tmp/sc_ver || true
  echo "--- sc inspect available via CLI help? ---"
  sc --help 2>&1 | rg -n "inspect|install|Native|native|plugins" || true
fi
if command -v sc-zig >/tmp/sc_zig_bin 2>/tmp/sc_zig_err; then
  bin="$(cat /tmp/sc_zig_bin)"
  echo "SC_ZIG_BIN=$bin"
  sc-zig --version 2>/tmp/sc_zig_ver || true
  sc-zig --help 2>&1 | rg -n "inspect|install|Native|native|plugins" || true
fi

echo
echo "== repo source inspection for sc supercli aliases =="
rg -n "function sc|alias sc|bin[[:space:]]*=|supercli|SC_CLI|super-cli" -S --glob '!node_modules' --glob '!dist' --glob '!build' | head -200

Repository: javimosch/supercli

Length of output: 32023


Use sc for native plugin installation.

plugins/inbox-watch/plugin.json is a new bundled plugin, and the installation step still uses supercli plugins install. Node.js sc is the native plugin-install client, so update this to sc plugins install ./plugins/inbox-watch --on-conflict replace --json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/inbox-watch/plugin.json` at line 22, Update the installation command
in the inbox-watch plugin configuration to use the native sc client instead of
supercli, preserving the existing plugins install arguments and path.

Source: Coding guidelines

],
"note": "Single Python 3 file, no dependencies. Config holds what to watch; env vars hold every secret (INBOX_TOKEN / RESEND_API_KEY / ZOHO_IMAP_PASS)."
},
"learn": {
"file": "skills/quickstart/SKILL.md"
},
"commands": [
{
"namespace": "inbox-watch",
"resource": "inbox",
"action": "check",
"description": "Poll every configured channel and print items new since the last run. Exit 10 if any. CONSUMES the cursor \u2014 use inbox_peek to diagnose.",
"adapter": "process",
"adapterConfig": {
"command": "inbox-watch",
"baseArgs": [
"--exit-zero"
],
"missingDependencyHelp": "Install: see https://github.com/javimosch/inbox-watch",
"note": "--exit-zero because a non-zero exit is treated as failure by the process adapter, and inbox-watch's exit 10 means 'new items' \u2014 a success. Read data.count instead."
},
"args": []
},
{
"namespace": "inbox-watch",
"resource": "inbox",
"action": "peek",
"description": "Same report WITHOUT marking items seen. Use this for any diagnostic run: a normal run consumes the alert your cron would have sent.",
"adapter": "process",
"adapterConfig": {
"command": "inbox-watch",
"baseArgs": [
"--peek",
"--exit-zero"
],
"missingDependencyHelp": "Install: see https://github.com/javimosch/inbox-watch",
"note": "--exit-zero because a non-zero exit is treated as failure by the process adapter, and inbox-watch's exit 10 means 'new items' \u2014 a success. Read data.count instead."
},
"args": []
},
{
"namespace": "inbox-watch",
"resource": "inbox",
"action": "seed",
"description": "Mark everything currently pending as seen, reporting nothing. Run once when adding a new consumer, or it alerts on the whole backlog.",
"adapter": "process",
"adapterConfig": {
"command": "inbox-watch",
"baseArgs": [
"--seed",
"--exit-zero"
],
"missingDependencyHelp": "Install: see https://github.com/javimosch/inbox-watch",
"note": "--exit-zero because a non-zero exit is treated as failure by the process adapter, and inbox-watch's exit 10 means 'new items' \u2014 a success. Read data.count instead."
},
"args": []
},
{
"namespace": "inbox-watch",
"resource": "self",
"action": "guide",
"description": "Embedded operator manual: the model, the loop, and the gotchas",
"adapter": "process",
"adapterConfig": {
"command": "inbox-watch",
"baseArgs": [
"guide"
],
"missingDependencyHelp": "Install: see https://github.com/javimosch/inbox-watch"
},
"args": []
},
{
"namespace": "inbox-watch",
"resource": "self",
"action": "help-json",
"description": "Machine-readable command catalog (cli-output-spec)",
"adapter": "process",
"adapterConfig": {
"command": "inbox-watch",
"baseArgs": [
"help-json"
],
"missingDependencyHelp": "Install: see https://github.com/javimosch/inbox-watch"
},
"args": []
},
{
"namespace": "inbox-watch",
"resource": "_",
"action": "_",
"description": "Passthrough to the inbox-watch CLI",
"adapter": "process",
"adapterConfig": {
"command": "inbox-watch",
"passthrough": true,
"missingDependencyHelp": "Install: see https://github.com/javimosch/inbox-watch"
},
"args": []
Comment on lines +110 to +121

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Normalize exit 10 for passthrough too.

This exposed route omits --exit-zero; a passthrough poll that finds new items exits 10 and is surfaced as a process-adapter failure. Add the same baseArgs normalization and cover it with a passthrough test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/inbox-watch/plugin.json` around lines 110 - 121, Update the exposed
passthrough route’s process adapter configuration for the “inbox-watch” command
to include the same baseArgs exit-code normalization used by the non-passthrough
route, including --exit-zero. Add or extend a passthrough test to verify that a
poll returning exit code 10 is normalized successfully rather than reported as
an adapter failure.

}
]
}
96 changes: 96 additions & 0 deletions plugins/inbox-watch/skills/quickstart/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
name: inbox-watch-quickstart
description: Poll GitHub, IMAP and Resend inbound for genuinely new items and alert only when a human actually replied. Use when setting up reply monitoring, wiring a cron/timer to an inbox, or debugging why an expected alert never fired.
---

# inbox-watch

**Tells you only when a human actually replied.** Polls GitHub notifications, IMAP
and Resend inbound; prints what is new since the last run; **exits 10** if there
was anything — so a cron alerts only when something real arrived.
Comment on lines +8 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the public alert contract for autoreplies.

The descriptions promise alerts only for human replies, while the quickstart table says labelled autoreplies also alert. Choose the intended behavior and make every public surface match.

  • plugins/inbox-watch/skills/quickstart/SKILL.md#L8-L10: revise the “only when a human replied” claim.
  • plugins/inbox-watch/skills/quickstart/SKILL.md#L19-L23: align the autoreply alert row with the chosen contract.
  • plugins/inbox-watch/skills/quickstart/SKILL.md#L3-L3: align front-matter description.
  • plugins/inbox-watch/meta.json#L2-L2: align plugin metadata description.
  • plugins/inbox-watch/plugin.json#L4-L4: align manifest description.
📍 Affects 3 files
  • plugins/inbox-watch/skills/quickstart/SKILL.md#L8-L10 (this comment)
  • plugins/inbox-watch/skills/quickstart/SKILL.md#L19-L23
  • plugins/inbox-watch/skills/quickstart/SKILL.md#L3-L3
  • plugins/inbox-watch/meta.json#L2-L2
  • plugins/inbox-watch/plugin.json#L4-L4
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/inbox-watch/skills/quickstart/SKILL.md` around lines 8 - 10, Adopt
the contract that both human replies and labelled autoreplies trigger alerts,
then align every public description accordingly: update
plugins/inbox-watch/skills/quickstart/SKILL.md lines 8-10, 19-23, and 3;
plugins/inbox-watch/meta.json line 2; and plugins/inbox-watch/plugin.json line
4. Remove wording that limits alerts to human replies and ensure the autoreply
row and all metadata consistently describe the selected behavior.


## The one idea

Inbound is mostly machine noise. On a domain with DMARC reporting on, aggregate
reports outnumber real mail several to one. A notifier that pings for all of it
gets muted — **and then the reply that mattered is muted with it.** So every item
is classified and only one kind is a person:

| kind | alert? |
|---|---|
| `dmarc` | no — counted, never pinged alone |
| `autoreply` | yes, but **labelled** so it doesn't read as a reply |
| `reply` | **yes** — this is the product |

## Use

```sh
inbox-watch # JSON on stdout, exit 10 if new
inbox-watch --human # readable lines
inbox-watch guide # embedded operator manual
inbox-watch help-json # command catalog
inbox-watch setup # what each channel needs
```

Output follows [cli-output-spec](https://cli-specs.intrane.fr/): stdout is
versioned data, stderr is context and typed errors, exit code is the signal.

```json
{ "ok": true, "version": "1.0.0", "count": 1,
"new": [ { "channel": "mailbox", "kind": "reply", "from": "…", "title": "…" } ],
"disabled": [ { "channel": "resend", "reason": "set RESEND_API_KEY …", "recoverable": true } ] }
```

| exit | meaning |
|---:|---|
| 0 | nothing new |
| **10** | **new items — the cron signal** |
| 80 / 90 / 100 / 110 | input / precondition / external / internal |

## Two traps — read these before debugging

Both were found in production, and both make a *working* system look broken (or
a broken one look fine).

**1. The cursor is consumed on read.** Any normal run marks items seen. So
running `inbox-watch` by hand to "check if it works" **eats the alert the cron
would have sent** — and the cron then reports nothing, which looks like success.

> Use `--peek` for anything diagnostic. It reports identically without
> committing state.

**2. Two watchers on one host steal from each other.** Same cause: they share a
cursor, so whichever polls first wins and the other's alert never fires.

> Give each its own: `--consumer NAME`. Run `--seed` once for a new consumer, or
> its first run alerts on the entire backlog.

```sh
inbox-watch --peek --human # safe diagnosis
inbox-watch --consumer laptop --seed # add a second watcher, no backlog flood
inbox-watch --consumer laptop
```

## Configure

`~/.inbox-watch/config.json` holds **what to watch**; env vars hold **every
secret**. Start from `config.example.json`. Unconfigured channels report why
they are disabled rather than failing, so a fresh install runs and tells you
what it needs.

| channel | secret | note |
|---|---|---|
| `mailbox` | `INBOX_TOKEN` + `mailbox.url` | token-gated `GET /api/inbound`. **Prefer this over `resend`** — a read-only token cannot send mail as you |
| `resend` | `RESEND_API_KEY` | note this key *can also send* |
| `imap` | `ZOHO_IMAP_PASS` + `imap.user` | any IMAP host, app-specific password |
| `github` | authenticated `gh` | mentions/review-requests, plus specific issue/PR threads you name |

## Wire it up

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Declare the cron snippet as shell.

Use ```sh so Markdown tooling and renderers classify the example correctly.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 90-90: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/inbox-watch/skills/quickstart/SKILL.md` at line 90, Update the cron
example’s fenced code block in the quickstart documentation to use the sh
language identifier instead of an untyped fence, preserving the snippet content
unchanged.

Source: Linters/SAST tools

*/15 * * * * inbox-watch --human | grep . && <notify>
```

`run.sh.template` in the repo is a worked example (Telegram). It **logs the
delivery result** — a send that fails silently is the same failure mode this
tool exists to prevent, so don't discard it.
Loading