competitor-monitor
Watch a list of competitor web pages on a cadence - snapshots each page's real signals (pricing, headings, CTAs, new/removed pages, title/description), diffs against the last run, and reports only what actually changed.
git clone --depth 1 https://github.com/aeonfun/aeon /tmp/competitor-monitor && cp -r /tmp/competitor-monitor/skills/competitor-monitor ~/.claude/skills/competitor-monitorSKILL.md
Today is ${today}.
> **${var}** — the pages to watch, comma-separated.
> - **empty** → read the watch list from `memory/competitors.md`.
> - **`https://rival.com/pricing, https://rival.com`** → watch exactly these
> pages this run (a bare host gets `https://` prepended). Watch specific
> **pages**, not just origins: `/pricing`, `/changelog`, `/blog` are where a
> competitor's moves actually show up.
> - **`add:<url>`** → append `<url>` to `memory/competitors.md`, confirm, and end
> (the shape the Telegram force-reply sends). No monitor runs.
## What this does
Fetches each watched page, extracts the handful of signals a human would notice
if they reopened the tab — the pricing numbers, the section headings, the
call-to-action buttons, the nav/footer links, the `<title>` and meta description
— snapshots them, and diffs today's snapshot against the previous run. It reports
**only the differences**, ranked by how much they matter (a pricing change beats
a reworded button), and keeps a durable log of every change it has ever seen.
The heavy lifting is in `scripts/competitor-monitor.mjs`, which returns
machine-readable signals and a machine-computed diff, so you reason over facts
instead of eyeballing two HTML dumps. **Diffing signals, not raw HTML, is the
whole point** — raw HTML churns every deploy (build hashes, nonces, inlined
timestamps) and would fire on every run. Signals only move when the site
actually moved.
This reads what a page **serves**. A pure client-rendered SPA that ships an empty
shell will look thin — the meta tags and any server-rendered copy still diff, but
JS-injected content won't. Most marketing, pricing, blog, and changelog pages are
server-rendered enough to track; say so if a target comes back near-empty rather
than inventing signal.
## Capability notes (read before editing this skill)
This skill is `mode: read-only`, and that is load-bearing (same contract as
`seo-audit`):
- The scraper is **stdlib Node, not Python** — `Bash(node:*)` is in the read-only
capability base; `Bash(python3:*)` is write-tier. Porting to Python forces
`mode: write`.
- **No Write or Edit tool, and no shell output redirection.** Read-only mode
strips `Write`/`Edit` *and* the Bash permission layer blocks `> file` /
`>> file` as defense-in-depth. So every file this skill produces is written by
a `Bash(node:*)` command that opens the file itself — the snapshot/diff script
takes `--out FILE`, and `CHANGES.md` is written by piping its content into a
one-line `node` writer (both shown below). Do **not** use `>` — it will be
refused mid-run. The read-only guard reverts writes to code/config paths but
**preserves `memory/` and `output/`**, which is exactly where this skill writes.
- No secrets, no `requires:`. It only makes outbound HTTPS GETs.
## Workflow
### 1. Resolve the watch list
Parse `${var}`:
```bash
RAW="$(printf '%s' "${var}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
# Config capture (Telegram force-reply): var="add:<url>" appends to the watch list and ends.
case "$RAW" in
add:*)
CAND="$(printf '%s' "${RAW#add:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]].*$//')"
case "$CAND" in http://*|https://*) ;; *) CAND="https://$CAND" ;; esac
if ! printf '%s' "$CAND" | grep -qiE '^https?://[a-z0-9.-]+\.[a-z]{2,}(/.*)?$'; then
./notify "Couldn't read \"$CAND\" as a URL. Reply with a full page URL (e.g. https://rival.com/pricing)."
exit 0
fi
mkdir -p memory; touch memory/competitors.md
if grep -qiF "$CAND" memory/competitors.md; then
./notify "Already watching $CAND."
else
printf -- '- %s\n' "$CAND" >> memory/competitors.md
./notify "Now watching $CAND — it'll show up in the next Competitor Monitor run."
fi
exit 0 ;;
esac
```
If `$RAW` is non-empty, the targets are `$RAW` split on commas (trim each). If
`$RAW` is empty, read the watch list from `memory/competitors.md`:
```markdown
# memory/competitors.md
- https://rival.com
- https://rival.com/pricing
- https://rival.com/changelog
- https://othercompetitor.com/pricing
```
If the file is missing or empty **and** `$RAW` is empty, offer to seed it via a
Telegram force-reply, but **only if no `add` prompt was already offered in the
last 3 days of `memory/logs/`** (don't nag an unconfigured fork every run):
```bash
./notify "No competitor pages on the watch list yet. Which page should I watch? Reply with a full URL." \
--force-reply --placeholder "https://rival.com/pricing" \
--context "competitor-monitor::add"
```
Then log `COMPETITOR_MONITOR_EMPTY_CONFIG` and end. The reply routes back as
`var=add:<url>`, handled above.
### 2. Snapshot every page
Write one timestamped snapshot per run — never overwrite an earlier one. The
script writes the file itself with `--out` (read-only mode blocks `>`); **never
retype its JSON** (a hand-copied snapshot is where invented numbers come from, and
it's the file the next diff trusts):
```bash
mkdir -p memory/competitor-monitor
STAMP=$(date -u +%Y-%m-%dT%H-%M-%SZ)
node scripts/competitor-monitor.mjs snapshot <url1> <url2> ... --out "memory/competitor-monitor/${STAMP}.json"
```
Pass the targets as arguments (`--out` may sit anywhere in the args). The script
fetches sequentially (polite; watch lists are short), follows redirects, and marks
any page that failed with `"ok": false` + an `error` — it does not abort the run
for one dead page. It exits non-zero only if **every** page failed.
### 3. Diff against the previous run
The baseline is the **newest snapshot that already exists** — i.e. the previous
run's, since this run wrote its file in step 2. Exclude the file you just wrote:
```bash
CUR="memory/competitor-monitor/${STAMP}.json"
PREV=$(ls -1 memory/competitor-monitor/*.json 2>/dev/null | grep -vF "$CUR" | sort | tail -1)
```
If `$PREV` is empty, this is the **first run** — there is nothing to diff. Send a
one-line baseline note (`Competitor Monitor — tracking N page(s), baseline sSet up and run an Aeon agent instance — get started from scratch, pick which skills to turn on or install more from packs, reschedule or change what runs, edit what an existing skill does, fix a skill that isn't firing, set the STRATEGY.md north star and soul/ voice, turn a coding-agent chat into a scheduled Aeon skill, and mine past coding-agent conversations for recurring work worth automating as a skill. Use when the user mentions Aeon, aeon.yml, an Aeon skill / instance / routine / pack, asks to schedule, enable, edit, or debug an agent that runs on a cron, or asks what of their repeated/manual work Aeon could take over.
Mention/keyword sweep on social platforms for [REPLACE: KEYWORDS] — trends, sentiment, top posts
5 concrete real-life actions, leverage-scored against open loops with specificity and anti-fluff gates
Static config-correctness linter for this instance - catches the silent-failure class (unquoted schedules, duplicate keys, unconfigured skills, mode typos, broken requires/MCP refs) that no run-based health skill can see. Notifies only on problems.
Pull framework updates from the upstream Aeon repo into this instance - 3-way merges canon's new commits into a PR, never clobbering operator config.
Write a publication-ready article in one of three angles - a trending long-form piece, a watched-repo thesis, or a project-through-a-lens essay. Optional Replicate hero image with --visual.
Automatically merge open PRs that have passing CI, no blocking reviews, and no conflicts
Two-mode aeon.yml workflow builder - analyze inspects URLs and emits a tiered, signal-verified skill-enablement plan plus an aeon.yml diff; enable flips slugs to enabled:true and opens a PR.