competitor-intel
Skill6 starsChanged 48 days agoMITView on GitHub
Scan competitor websites and ad libraries, detect what changed since the last scan, and write a competitive briefing. Use when the user says "/competitor-intel", "competitor scan", "what are our competitors doing", "competitive intelligence", "competitor briefing", or asks to track a competitor's website or ads over time. First run walks through a short intake that stores company and competitor details in a competitor-intel/ folder in the current project — no company data lives in the skill. Works with no dependencies (curl); adds screenshots and LinkedIn ad extraction when Node and Playwright are available.
9 other projects use a version of this.
Adopted by
Most recently taken up by
- Alessandro114/scala-mcp-servertheir own version, first seen Nov 8, 2026
- akii-technologies-ltd/akii-seo-ai-search-optimizertheir own version, first seen Nov 1, 2026
- Amazon-Quick/Amazon-Quick-official-catalogtheir own version, first seen Oct 28, 2026
- manifoldmcp/marketing-skillstheir own version, first seen Oct 17, 2026
- Nimbleway/agent-skillstheir own version, first seen Aug 11, 2026
- ekinciio/saas-growth-marketing-skillstheir own version, first seen Jul 14, 2026
Other versions9 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 9
- hyperfx-ai/marketing-skills★ 112same name
- akii-technologies-ltd/akii-seo-ai-search-optimizer★ 79same name
- Nimbleway/agent-skills★ 61same name
- Amazon-Quick/Amazon-Quick-official-catalog★ 50same name
- ekinciio/saas-growth-marketing-skills★ 12same name
- manifoldmcp/marketing-skills★ 4same name
- Alessandro114/scala-mcp-server★ 2same name
- KamalikaPoddar/skills★ 1same name
- massielvasquez193-dot/Claude-skill★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository19 other files it gives its agents.
- campaign-reportSkill
- content-auditSkill
- content-strategySkill
- croSkill
- customer-researchSkill
- lead-magnetsSkill
- linkedin-adsSkill
- marketing-councilSkill
- marketing-monthlySkill
- pipeline-analysisSkill
- product-launchSkill
- internal-linkingSkill
- seo-reportSkill
- beautify-github-readmeSkill
- cinemagraphSkill
- frontend-design-anti-slopSkill
- copy-deslopSkill
- copy-longformSkill
- writing-assistantSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
15 rules. 0 are common, 4 are distinctive, and 11 are very specific to this project.
0
Common
Most of the 9 edited versions we read keep these.
4
Distinctive
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 21
Check for competitor-intel/company.md in the current working directory.
- Distinctive: line 40
Ask first, before anything else:
- Very specific: line 48
(a) Point me at files: Never assume a competitor-intel/ or context folder already holds the right thing.
- Distinctive: line 55
(b) From scratch: Ask one question at a time, in this order:
- Very specific: line 112
Read competitor-intel/config.json and competitor-intel/competitors.md.
- Distinctive: line 182
If the command itself fails — non-zero exit, or no output at all — fall back to the default tier for the whole run, say so once, and note …
- Very specific: line 186
Fill
references/snapshot-schema.jsonliterally: Every key present, no extras, and an empty value written as "", 0, or [] — never omitted, … - Very specific: line 212
Write this to competitor-intel/runs/<stamp>/snapshot.json, where <stamp> is YYYY-MM-DD_HH-mm.
- Very specific: line 214
If a previous run directory exists under competitor-intel/runs/, apply references/diff-rules.md in full — including its §6 schema-drift …
- Very specific: line 219
Read only snapshot.json and changes.json for this — never go back to raw HTML or the fetch output — and write …
- Very specific: line 222
Update competitor-intel/config.json's lastRun to the run stamp.
- Distinctive: line 232
Before you send that final message, check your last paragraph: the turn ends with the briefing, not a plan to write one.
- Very specific: line 237
Every per-URL failure — a non-200 STATUS:, a transport error, a timeout — becomes one entry in that competitor's errors[], shaped { "url", …
- Very specific: line 257
If a competitor's assembled snapshot exceeds 60 KB (the hard guard.
- Very specific: line 268
If competitor-intel/runs/ doesn't exist yet, or is empty, or the named run-id doesn't exist: don't scan and don't fabricate a briefing.
“Common” is measured: kept by at least 6 in 10 of the 9 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Competitor Intel
Scans competitor websites (and, at the deep tier, LinkedIn ad libraries), diffs the result
against the last scan, and writes a briefing a person actually reads. Everything the skill
learns about a company lives in a competitor-intel/ folder inside the current project — never
in this skill folder, never in any repo this skill ships from.
Paths in this file. scripts/… and references/… are relative to this skill's
folder. competitor-intel/… is relative to the current working directory — the user's
project. This skill deliberately works in two roots at once, so every script invocation below
needs the skill folder's own path in front of it (written <skill>/scripts/…), while
competitor-intel/… arguments stay relative to the project.
Routing
Check for competitor-intel/company.md in the current working directory.
- Missing → run Intake.
- Present, user said
scanor gave no argument → run Scan. - Present, user said
setup→ re-run Intake against the existing files (confirm before overwriting). - Present, user said
report [run-id]→ run Report (no re-scraping — read the existingruns/<run-id>/snapshot.jsonandchanges.json, or the latest run ifrun-idis omitted, and regeneratebriefing.mdfrom them perreferences/briefing-format.md).
Intake
Always ask the intake question, even when the project looks pre-loaded and the setup seems obvious. Do not infer company or competitor details from the repo, from CLAUDE.md, or from earlier conversation and skip straight to writing files. The user's own words — how they name their competitors, what they think changed, which one worries them — shape the whole briefing downstream, and skipping the question throws that away even when you could technically guess right.
Ask first, before anything else:
"Setting up competitor tracking. Do you want to point me at files you already have (a competitor list, positioning doc, past research), or start from scratch — one question at a time?"
Offer this as an AskUserQuestion with two paths:
(a) Point me at files. Never assume a competitor-intel/ or context folder already holds
the right thing. Scan the current launch directory for likely candidates (a competitor list,
CLAUDE.md/AGENTS.md, a context/ folder, anything with "competitor," "positioning," or
"research" in the name). List what you found by filename only — don't read contents yet — then
ask the user to confirm one of: use these / redirect me to other files / paste it instead /
none of this, start from scratch. Wait for the answer, then read only what was approved.
(b) From scratch. Ask one question at a time, in this order:
- Company name and URL (this is your company, for framing — not tracked as a competitor).
- Competitors to track: name + URL for each.
- For each competitor, the LinkedIn account name — but only ask this when it would differ from the company name. State why when you ask: a name-similar company on LinkedIn is an easy false match, and matching the wrong account silently pollutes the ad data with someone else's ads with no signal that it happened. If the company name is a safe-enough guess for the LinkedIn handle, don't force the question — confirm the guess instead.
From the answers, write:
competitor-intel/company.md — the user's own company name, URL, and any framing notes
they gave.
competitor-intel/competitors.md — one line per competitor, this exact format:
- Name | https://url/ | linkedin-account-nameValidate every URL before you write this file, whichever intake path produced it: it must
match ^https?://[A-Za-z0-9.:-]+ — an http/https scheme and a host of letters, digits,
dots, hyphens and an optional :port, nothing else. If a URL doesn't, say which one and ask
for a corrected value rather than writing it. competitors.md is hand-edited afterwards and
meant to be committed and shared across a team, so it is not self-trusted input: it is the one
place a URL enters the scripts. Both scripts refuse a URL that fails this check (and refuse
file:// outright), so an unvalidated entry silently costs you that competitor's whole scan.
competitor-intel/config.json — this exact default, before any user override:
{ "tier": "auto", "maxSubpages": 10, "locale": "en-US", "lastRun": "" }Then, regardless of which path was taken, check for the deep tier: is node on PATH, and is
it Node 20 or newer? Check the version, not just the presence — node -v — because
Playwright 1.63.0 declares engines.node >= 20 and will not install cleanly below it. If yes,
offer it — explain that the deep tier adds screenshots and real LinkedIn ad extraction via
Playwright, and that turning it on means running npm install and npx playwright install
chromium in the skill's scripts/deep/ folder, which downloads about 310 MB. Only do this
after the user says yes. If node is missing or older than 20, or the user declines, stay on
the default tier — tier: "auto" will still resolve to default at scan time (see Scan, and
Error handling below).
Scan
You are operating autonomously during a scan. The user is not watching in real time. For reversible steps that follow from the scan request, proceed without asking. Stop only for the deep-tier install (downloads ~310 MB) and for anything destructive.
The fetch loop below is mechanical and cheap — a script does the extraction, you just batch the calls and file the output. Don't spend judgment there. Save your attention for the briefing: that's the one artifact a person reads, and it's where materiality calls, wording, and the Quoting rule actually matter.
Procedure:
- Read
competitor-intel/config.jsonandcompetitor-intel/competitors.md. - Resolve the tier:
tier: "auto"means deep ifnodeis on PATH andnode -vreports major version 20 or newer andscripts/deep/node_modulesexists; otherwise default. If the config says"deep"but any part of that check fails, fall back to default and say so once (see Error handling). Per competitor, run
FETCH_LOCALE='<config.locale>' bash "<skill>/scripts/discover.sh" '<url>' <maxSubpages>Single-quote the URL. You are composing a Bash line from
competitor-intel/competitors.md, a file a human edits and a team shares; an unquoted URL containing;,&or a backtick would execute. Same rule everywhere below: step 4's URLs and step 5's paths.<maxSubpages>is config'smaxSubpages, default 10 — it capsotherpages only;servicesis uncapped,aboutandcase-studyare capped at 5 by the script itself.FETCH_LOCALEis config'slocale; it becomes theAccept-Languageheader on every default-tier fetch, anddiscover.shpasses it down tofetch.sh.The output, in order:
SOURCE: nav|sitemap|none— which path produced the primary candidate list. This is run provenance, not a page'ssource; never stamp it onto pages.CAPPED: about=N case-study=N other=N— present only when a cap actually dropped rows.Nis the number of rows dropped, not kept. Carry it into Data notes item 3.- Then one row per candidate:
<kind>\t<source>\t<url>, tab-separated.kindisservices,about,case-studyorother.sourceis that page's own provenance —navorsitemap, and only those two values (a page can only exist if one of the two lists produced it;navwins when both did).
- Batch the fetches. List what you need, then request every fetch that doesn't depend on
another's result in one response. Run
FETCH_LOCALE='<config.locale>' bash "<skill>/scripts/fetch.sh" '<url>' 3000for the homepage and the same with1500for each subpage. Each call emitsURL:,STATUS:,TITLE:,META:,H1:,HEADINGS:(pipe-separated),LINKS:(pipe-separated), andTEXT:(last, the only multi-word field) — orSTATUS:+ERROR:on a non-200 response, a transport failure, or a URL the script refuses, and the script still exits 0 in every one of those cases. Deep tier only — run the browser pass. Skip this step entirely on the default tier. Decide the run stamp now (
YYYY-MM-DD_HH-mm); step 6 writes to the same one. Run this once for the whole run, not once per competitor:node "<skill>/scripts/deep/scan.mjs" \ --competitors 'competitor-intel/competitors.md' \ --out 'competitor-intel/runs/<stamp>' \ --locale '<config.locale>'It prints one JSON object per line on stdout, one line per competitor, shaped
{ "name", "screenshots": { "home" }, "linkedinAds": { "tier", "count", "ads" }, "errors": [] }. Screenshots are written undercompetitor-intel/runs/<stamp>/screenshots/. For each line, find the matching competitor byname(the value echoed fromcompetitors.md) and merge into that competitor's snapshot as you assemble it in step 6:screenshots.home← the line'sscreenshots.home— a path relative to the run directory, e.g.screenshots/acme-analytics-home.png. Keep it relative; never rewrite it to an absolute path.linkedinAds← the line'slinkedinAdsobject entire (tier,count,ads[]).errors[]← append the line'serrors[]to whatever the fetch loop already recorded for that competitor. Never replace.- top-level
tier←"deep".
A competitor with no matching line (scan.mjs crashed partway, or the name didn't match) keeps its default-tier values —
tier: "default",linkedinAds.tier: "default",count: 0,ads: [],screenshots.home: ""— and gets a Data notes line saying the deep pass produced nothing for it.linkedinAds.tierbecomes"deep"only when the merge actually happened: it is the field that tells a reader whether the ad numbers came from a browser or from nothing at all, so never write"deep"onto a competitor whose merge didn't happen. Same for the top-leveltier.If the command itself fails — non-zero exit, or no output at all — fall back to the default tier for the whole run, say so once, and note it in Data notes. A failed deep pass never stops the scan.
- Fill
references/snapshot-schema.jsonliterally. Every key present, no extras, and an empty value written as"",0, or[]— never omitted, nevernull. This is the same rigidity contractdiff-rules.md§0 states for the diff step, restated here because a schema you don't re-read at fill time is a schema you drift from:- Top level:
name,url,fetchedAt,tier,home,pages,linkedinAds,screenshots,errors. home:title,metaDescription,h1,headings(array),text(≤3000 chars).- Each
pages[]entry:url,kind,source,title,h1,text(≤1500 chars) — nometaDescriptionkey on page entries, onlyhomehas one.urlandkindcome from columns 3 and 1 of that page'sdiscover.shrow;sourcecomes from column 2 of the same row — that page's own provenance,navorsitemap. Never derive it from the globalSOURCE:line: stamping that label on every page makes every page readnavwhenever nav returned anything, which silently empties the briefing's "not in nav" bullet and quietly folds sitemap-only pages into its "from nav" row. The briefing asserts this provenance per page twice; it has to be observed, not assumed. linkedinAds:tier,count,ads[]— each ad:headline,body,firstSeen. On the default tier this stays at the schema's own defaults —tier: "default",count: 0,ads: []— the default tier has no ad-fetch path at all, so there is nothing to populate, symmetric withscreenshotsstaying empty below. On the deep tier it is whatever step 5 merged in for that competitor, and nothing at all if step 5 produced no line for it.screenshots:home,pages(object keyed by page URL) — leave paths empty on the default tier. On the deep tier,homeis step 5's run-relative path (screenshots/<name>-home.png) andpagesstays{}:scan.mjsscreenshots the homepage only.errors: one entry per failed fetch, each{ "url", "status", "note" }(see Error handling for what goes in each field). Write this tocompetitor-intel/runs/<stamp>/snapshot.json, where<stamp>isYYYY-MM-DD_HH-mm.
- Top level:
- If a previous run directory exists under
competitor-intel/runs/, applyreferences/diff-rules.mdin full — including its §6 schema-drift guard — to producecompetitor-intel/runs/<stamp>/changes.json. If there is no previous run, or a competitor's own comparison fails the drift guard, write the baseline form per diff-rules.md §5/§6 instead of skipping the file. - Read only
snapshot.jsonandchanges.jsonfor this — never go back to raw HTML or the fetch output — and writecompetitor-intel/runs/<stamp>/briefing.mdperreferences/briefing-format.md, all five sections, in order. - Update
competitor-intel/config.json'slastRunto the run stamp.
Scope: scan, diff, brief. If you find a bug in the scripts, or notice a page you could also be scraping, note it as a follow-up at the end of the briefing's Data notes — don't fix or extend anything mid-scan.
Progress: say in one line what you're about to do before you start; one short line per competitor as it completes; close with the briefing itself, not a description of it — the briefing must stand alone as the final message.
Before you send that final message, check your last paragraph: the turn ends with the briefing, not a plan to write one.
Error handling and size guard
- Every per-URL failure — a non-200
STATUS:, a transport error, a timeout — becomes one entry in that competitor'serrors[], shaped{ "url", "status", "note" }:url— the URL that failed, exactly as fetched.status— the HTTP status as an integer (e.g.403,404,500). For a transport failure with no HTTP response at all, use0— this isfetch.sh's own convention: it printsSTATUS: 000in that case (curl couldn't complete the request), and000becomes the integer0here, matching the schema's"status": 0default.note— a short human-readable reason (e.g."blocked with 403","timed out","transport failure"). The scan continues past it; a failed fetch never stops the run.
- A
403or429on the homepage or a subpage means: skip the rest of that competitor's subpages for this run, record it inerrors[], and note in the briefing's Data notes that the site blocked default-tier fetches. Be precise about the remedy: the deep tier does not recover the missing content.scan.mjstakes a homepage screenshot and reads the LinkedIn Ad Library — it extracts no title, h1, meta, text or links, so it cannot supply a single field the 403 cost you. What the deep tier buys here is a screenshot a person can read by eye. Say that, and recommend a manual check for the content itself. nodeis on PATH butscripts/deep/node_modulesis missing (Playwright never installed, or removed), ornode -vis older than 20: fall back to the default tier for this run and say so once, in one line, before the fetch loop starts — not once per competitor.- If a competitor's assembled snapshot exceeds 60 KB (the hard guard; target is 40 KB), truncate
that snapshot's
textfields — home first, then subpages, longest first — until it's under the guard, and note in Data notes which competitor was truncated and by roughly how much.
Report
report [run-id] never re-scrapes. Read competitor-intel/runs/<run-id>/snapshot.json and
changes.json (or the most recent run directory if run-id is omitted), and regenerate
briefing.md from them per references/briefing-format.md. Use this to re-render a briefing
after a formatting fix, or to review an older run without burning a new scan.
If competitor-intel/runs/ doesn't exist yet, or is empty, or the named run-id doesn't exist:
don't scan and don't fabricate a briefing. Say plainly that there's no run to report on yet, and
point to scan as the next step (naming the missing/requested run-id if one was given).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

