aivisible-fix 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
aivisible_fix/audit.py ADDED
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from urllib.parse import urljoin
6
+
7
+ from .crawlers import AI_CRAWLERS
8
+ from .fetch import fetch
9
+ from .robots import agent_status, parse_robots_txt
10
+
11
+ # Each check's point value. Weighted toward the two things that, per current
12
+ # published data, cause the overwhelming majority of AI-search invisibility:
13
+ # blocked crawlers and a missing sitemap/structured data trail to follow.
14
+ POINTS = {
15
+ "robots_reachable": 5,
16
+ "no_ai_crawlers_blocked": 40,
17
+ "llms_txt_present": 20,
18
+ "sitemap_present": 15,
19
+ "sitemap_referenced_in_robots": 5,
20
+ "structured_data_present": 15,
21
+ }
22
+
23
+
24
+ @dataclass
25
+ class Finding:
26
+ check: str
27
+ passed: bool
28
+ points: int
29
+ detail: str
30
+
31
+
32
+ @dataclass
33
+ class AuditReport:
34
+ base_url: str
35
+ findings: list[Finding] = field(default_factory=list)
36
+ blocked_agents: list[str] = field(default_factory=list)
37
+ score: int = 0
38
+
39
+ def add(self, check: str, passed: bool, detail: str) -> None:
40
+ points = POINTS[check] if passed else 0
41
+ self.findings.append(Finding(check, passed, points, detail))
42
+ self.score += points
43
+
44
+
45
+ def run_audit(base_url: str) -> AuditReport:
46
+ base_url = base_url.rstrip("/")
47
+ if not base_url.lower().startswith("https://"):
48
+ raise ValueError(
49
+ f"Refusing to audit '{base_url}': only https:// URLs are supported. "
50
+ "This tool never fetches over plaintext HTTP, for both your safety "
51
+ "and the target site's."
52
+ )
53
+ report = AuditReport(base_url=base_url)
54
+
55
+ robots_res = fetch(urljoin(base_url + "/", "robots.txt"))
56
+ if not robots_res.ok:
57
+ report.add("robots_reachable", False, "robots.txt missing or unreachable — AI crawlers default to allowed, but you have no way to signal intent or steer them to a sitemap.")
58
+ groups = []
59
+ else:
60
+ report.add("robots_reachable", True, "robots.txt found.")
61
+ groups = parse_robots_txt(robots_res.text)
62
+
63
+ blocked = []
64
+ for agent in AI_CRAWLERS:
65
+ status = agent_status(groups, agent)
66
+ if status == "blocked":
67
+ blocked.append(agent)
68
+ report.blocked_agents = blocked
69
+ if blocked:
70
+ report.add(
71
+ "no_ai_crawlers_blocked",
72
+ False,
73
+ f"{len(blocked)} AI crawler(s) explicitly blocked: {', '.join(blocked)}.",
74
+ )
75
+ else:
76
+ report.add("no_ai_crawlers_blocked", True, "No known AI crawler is explicitly blocked.")
77
+
78
+ llms_res = fetch(urljoin(base_url + "/", "llms.txt"))
79
+ report.add(
80
+ "llms_txt_present",
81
+ llms_res.ok and len(llms_res.text.strip()) > 0,
82
+ "llms.txt found." if llms_res.ok else "No /llms.txt — the emerging convention for telling an LLM what your site is and which pages matter most.",
83
+ )
84
+
85
+ sitemap_url = None
86
+ m = re.search(r"(?im)^sitemap:\s*(\S+)", robots_res.text) if robots_res.ok else None
87
+ if m:
88
+ sitemap_url = m.group(1).strip()
89
+ sitemap_res = fetch(sitemap_url or urljoin(base_url + "/", "sitemap.xml"))
90
+ sitemap_looks_valid = sitemap_res.ok and ("<urlset" in sitemap_res.text or "<sitemapindex" in sitemap_res.text)
91
+ report.add(
92
+ "sitemap_present",
93
+ sitemap_looks_valid,
94
+ "sitemap.xml found and looks valid." if sitemap_looks_valid else "No reachable/valid sitemap.xml — AI crawlers (and search engines) have no efficient way to discover your full page list.",
95
+ )
96
+ report.add(
97
+ "sitemap_referenced_in_robots",
98
+ m is not None,
99
+ "robots.txt points crawlers at your sitemap." if m else "robots.txt doesn't reference a Sitemap: line, even if sitemap.xml exists at the default path.",
100
+ )
101
+
102
+ home_res = fetch(base_url + "/")
103
+ has_jsonld = home_res.ok and bool(re.search(r'<script[^>]+type=["\']application/ld\+json["\']', home_res.text, re.I))
104
+ report.add(
105
+ "structured_data_present",
106
+ has_jsonld,
107
+ "JSON-LD structured data found on the homepage." if has_jsonld else "No JSON-LD structured data on the homepage — AI answer engines lean on schema.org markup to extract facts (org name, product, FAQ, etc.) reliably.",
108
+ )
109
+
110
+ return report
aivisible_fix/badge.py ADDED
@@ -0,0 +1,50 @@
1
+ """Static SVG badge generator — a shields.io-style badge, generated locally
2
+ and hosted by whoever runs the tool (no server of ours required). This is
3
+ the zero-account distribution mechanic: a site that scores well has a
4
+ concrete, embeddable reason to link back, the same way "Deploys on Vercel"
5
+ or CI-passing badges spread.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ LABEL = "AI-visible"
10
+
11
+
12
+ def _color_for(score: int) -> str:
13
+ if score >= 80:
14
+ return "#2E8B57" # allow-green
15
+ if score >= 50:
16
+ return "#E8A23A" # amber
17
+ return "#C6432E" # block-red
18
+
19
+
20
+ def _text_width(text: str) -> int:
21
+ # Rough monospace-ish estimate — good enough for a badge, no font
22
+ # metrics library required (this stays stdlib-only by design).
23
+ return max(6, int(len(text) * 6.7)) + 10
24
+
25
+
26
+ def generate_badge_svg(score: int) -> str:
27
+ value_text = f"{score}/100"
28
+ label_text = LABEL
29
+ label_w = _text_width(label_text)
30
+ value_w = _text_width(value_text)
31
+ total_w = label_w + value_w
32
+ color = _color_for(score)
33
+
34
+ return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{total_w}" height="20" role="img" aria-label="{label_text}: {value_text}">
35
+ <linearGradient id="s" x2="0" y2="100%">
36
+ <stop offset="0" stop-color="#fff" stop-opacity=".08"/>
37
+ <stop offset="1" stop-opacity=".08"/>
38
+ </linearGradient>
39
+ <clipPath id="r"><rect width="{total_w}" height="20" rx="3" fill="#fff"/></clipPath>
40
+ <g clip-path="url(#r)">
41
+ <rect width="{label_w}" height="20" fill="#3a3f3c"/>
42
+ <rect x="{label_w}" width="{value_w}" height="20" fill="{color}"/>
43
+ <rect width="{total_w}" height="20" fill="url(#s)"/>
44
+ </g>
45
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
46
+ <text x="{label_w / 2}" y="14">{label_text}</text>
47
+ <text x="{label_w + value_w / 2}" y="14">{value_text}</text>
48
+ </g>
49
+ </svg>
50
+ '''
aivisible_fix/cli.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from urllib.parse import urljoin
6
+
7
+ from .audit import run_audit
8
+ from .badge import generate_badge_svg
9
+ from .fetch import fetch
10
+ from .fix import generate_llms_txt, generate_robots_patch
11
+
12
+
13
+ def cmd_audit(args: argparse.Namespace) -> int:
14
+ try:
15
+ report = run_audit(args.url)
16
+ except ValueError as e:
17
+ print(f"Error: {e}", file=sys.stderr)
18
+ return 2
19
+ print(f"\nAI-visibility audit for {report.base_url}")
20
+ print(f"Score: {report.score}/100\n")
21
+ for f in report.findings:
22
+ mark = "PASS" if f.passed else "FAIL"
23
+ print(f" [{mark}] {f.check}: {f.detail}")
24
+ if report.blocked_agents:
25
+ print(f"\nBlocked crawlers: {', '.join(report.blocked_agents)}")
26
+ print()
27
+ return 0 if report.score >= 70 else 1
28
+
29
+
30
+ def cmd_fix(args: argparse.Namespace) -> int:
31
+ try:
32
+ report = run_audit(args.url)
33
+ except ValueError as e:
34
+ print(f"Error: {e}", file=sys.stderr)
35
+ return 2
36
+ robots_res = fetch(urljoin(report.base_url + "/", "robots.txt"))
37
+ existing = robots_res.text if robots_res.ok else ""
38
+
39
+ patched_robots = generate_robots_patch(existing, report)
40
+ robots_out = args.out_robots or "robots.fixed.txt"
41
+ with open(robots_out, "w") as fh:
42
+ fh.write(patched_robots)
43
+ print(f"Wrote {robots_out}")
44
+
45
+ if not any(f.check == "llms_txt_present" and f.passed for f in report.findings):
46
+ llms_out = args.out_llms or "llms.txt"
47
+ llms_txt = generate_llms_txt(
48
+ site_name=args.name or report.base_url,
49
+ site_description=args.description or f"{args.name or report.base_url} — description needed, edit this file.",
50
+ key_pages=[],
51
+ )
52
+ with open(llms_out, "w") as fh:
53
+ fh.write(llms_txt)
54
+ print(f"Wrote {llms_out} (starter — edit key pages in before publishing)")
55
+
56
+ print("\nReview both files, then deploy them at your site root. Nothing was auto-published.")
57
+ return 0
58
+
59
+
60
+ def cmd_badge(args: argparse.Namespace) -> int:
61
+ try:
62
+ report = run_audit(args.url)
63
+ except ValueError as e:
64
+ print(f"Error: {e}", file=sys.stderr)
65
+ return 2
66
+ svg = generate_badge_svg(report.score)
67
+ out = args.out or "aivisible-badge.svg"
68
+ with open(out, "w") as fh:
69
+ fh.write(svg)
70
+ print(f"Wrote {out} (score {report.score}/100). Host it yourself and embed:")
71
+ print(f' <a href="https://example.com"><img src="/aivisible-badge.svg" alt="AI-visible: {report.score}/100"></a>')
72
+ return 0
73
+
74
+
75
+ def main(argv: list[str] | None = None) -> int:
76
+ parser = argparse.ArgumentParser(prog="aivisible-fix", description="Audit and fix a site's visibility to AI crawlers/answer engines.")
77
+ sub = parser.add_subparsers(dest="command", required=True)
78
+
79
+ p_audit = sub.add_parser("audit", help="Score a site's AI-crawler visibility")
80
+ p_audit.add_argument("url", help="e.g. https://example.com")
81
+ p_audit.set_defaults(func=cmd_audit)
82
+
83
+ p_fix = sub.add_parser("fix", help="Generate a patched robots.txt and starter llms.txt")
84
+ p_fix.add_argument("url")
85
+ p_fix.add_argument("--name", help="Site/company name for llms.txt")
86
+ p_fix.add_argument("--description", help="One-line description for llms.txt")
87
+ p_fix.add_argument("--out-robots", help="Output path for patched robots.txt")
88
+ p_fix.add_argument("--out-llms", help="Output path for generated llms.txt")
89
+ p_fix.set_defaults(func=cmd_fix)
90
+
91
+ p_badge = sub.add_parser("badge", help="Generate a static embeddable score badge (SVG)")
92
+ p_badge.add_argument("url")
93
+ p_badge.add_argument("--out", help="Output path for the badge SVG")
94
+ p_badge.set_defaults(func=cmd_badge)
95
+
96
+ args = parser.parse_args(argv)
97
+ return args.func(args)
98
+
99
+
100
+ if __name__ == "__main__":
101
+ sys.exit(main())
@@ -0,0 +1,24 @@
1
+ # Known AI crawler / agent user-agent tokens, as used in robots.txt.
2
+ # Kept as a single source of truth so audit.py and fix.py never drift apart.
3
+ # Sources: each vendor's own published crawler docs (OpenAI, Anthropic,
4
+ # Perplexity, Google, Common Crawl, ByteDance, Amazon, Apple, Cohere, Meta).
5
+
6
+ AI_CRAWLERS = {
7
+ "GPTBot": "OpenAI — trains/improves models on crawled content",
8
+ "ChatGPT-User": "OpenAI — fetches pages a ChatGPT user links to",
9
+ "OAI-SearchBot": "OpenAI — powers ChatGPT search result citations",
10
+ "ClaudeBot": "Anthropic — trains/improves Claude on crawled content",
11
+ "Claude-Web": "Anthropic — fetches pages referenced in a Claude chat",
12
+ "anthropic-ai": "Anthropic — legacy training crawler token",
13
+ "PerplexityBot": "Perplexity — indexes content for AI search answers",
14
+ "Perplexity-User": "Perplexity — fetches pages a user asked about",
15
+ "Google-Extended": "Google — controls use in Gemini / AI Overviews (separate from Googlebot)",
16
+ "CCBot": "Common Crawl — public dataset used to train many LLMs",
17
+ "Bytespider": "ByteDance — crawls for AI training",
18
+ "Amazonbot": "Amazon — crawls for Alexa/AI features",
19
+ "Applebot-Extended": "Apple — controls use in Apple Intelligence (separate from Applebot)",
20
+ "cohere-ai": "Cohere — trains/improves models on crawled content",
21
+ "Meta-ExternalAgent": "Meta — crawls for AI training and search features",
22
+ "Diffbot": "Diffbot — powers third-party AI/knowledge-graph products",
23
+ "YouBot": "You.com — indexes content for AI search answers",
24
+ }
aivisible_fix/fetch.py ADDED
@@ -0,0 +1,33 @@
1
+ """Tiny stdlib-only HTTP helper — no third-party deps required to run this tool."""
2
+ from __future__ import annotations
3
+
4
+ import urllib.error
5
+ import urllib.request
6
+ from dataclasses import dataclass
7
+
8
+ USER_AGENT = "aivisible-fix/0.1 (+https://github.com/; audit tool, respects robots.txt itself)"
9
+
10
+
11
+ @dataclass
12
+ class FetchResult:
13
+ ok: bool
14
+ status: int | None
15
+ text: str
16
+
17
+
18
+ def fetch(url: str, timeout: float = 10.0) -> FetchResult:
19
+ if not url.lower().startswith("https://"):
20
+ # Refuse plaintext HTTP: this tool fetches third-party sites and must
21
+ # not be usable to relay/downgrade requests over an unencrypted
22
+ # channel, and it never has a legitimate reason to fetch anything
23
+ # other than a public https:// URL.
24
+ return FetchResult(ok=False, status=None, text="")
25
+ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
26
+ try:
27
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
28
+ body = resp.read().decode("utf-8", errors="replace")
29
+ return FetchResult(ok=200 <= resp.status < 300, status=resp.status, text=body)
30
+ except urllib.error.HTTPError as e:
31
+ return FetchResult(ok=False, status=e.code, text="")
32
+ except (urllib.error.URLError, TimeoutError, OSError):
33
+ return FetchResult(ok=False, status=None, text="")
aivisible_fix/fix.py ADDED
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from .audit import AuditReport
4
+ from .crawlers import AI_CRAWLERS
5
+
6
+
7
+ def generate_robots_patch(existing_robots_text: str, report: AuditReport) -> str:
8
+ """Return a full replacement robots.txt: keeps every line the site already
9
+ had, and appends an explicit Allow-all group for each blocked AI crawler.
10
+ Non-destructive by construction — nothing existing is deleted or edited.
11
+ """
12
+ if not report.blocked_agents:
13
+ return existing_robots_text
14
+
15
+ lines = existing_robots_text.rstrip("\n").splitlines() if existing_robots_text.strip() else []
16
+ lines.append("")
17
+ lines.append("# --- Added by aivisible-fix: explicitly allow AI crawlers ---")
18
+ lines.append("# Remove any of these blocks if you deliberately don't want that")
19
+ lines.append("# crawler training on / citing your content.")
20
+ for agent in report.blocked_agents:
21
+ lines.append("")
22
+ lines.append(f"User-agent: {agent}")
23
+ lines.append("Allow: /")
24
+ return "\n".join(lines) + "\n"
25
+
26
+
27
+ def generate_llms_txt(site_name: str, site_description: str, key_pages: list[tuple[str, str]]) -> str:
28
+ """key_pages: list of (title, url) tuples for the handful of pages worth
29
+ pointing an LLM at directly (docs, pricing, about) — per the llms.txt spec
30
+ (llmstxt.org): an H1, a one-line summary, then link sections.
31
+ """
32
+ lines = [f"# {site_name}", "", f"> {site_description}", ""]
33
+ if key_pages:
34
+ lines.append("## Key pages")
35
+ for title, url in key_pages:
36
+ lines.append(f"- [{title}]({url})")
37
+ lines.append("")
38
+ lines.append(
39
+ "<!-- Generated by aivisible-fix. Edit freely — this is a starting point, "
40
+ "not a final file. Keep it short: link out rather than duplicating content. -->"
41
+ )
42
+ return "\n".join(lines) + "\n"
43
+
44
+
45
+ def suggest_jsonld_snippet(site_name: str, site_url: str, description: str) -> str:
46
+ return f"""<script type="application/ld+json">
47
+ {{
48
+ "@context": "https://schema.org",
49
+ "@type": "Organization",
50
+ "name": "{site_name}",
51
+ "url": "{site_url}",
52
+ "description": "{description}"
53
+ }}
54
+ </script>"""
55
+
56
+
57
+ def summarize_unknown_crawler_coverage() -> str:
58
+ return (
59
+ f"This tool checks {len(AI_CRAWLERS)} known AI crawler user-agents. "
60
+ "New ones ship periodically — re-run the audit monthly rather than treating "
61
+ "a clean report as permanent."
62
+ )
@@ -0,0 +1,80 @@
1
+ """Minimal robots.txt parser that groups directives by user-agent.
2
+
3
+ Deliberately not using urllib.robotparser: that API answers "can THIS
4
+ one agent fetch THIS one URL", but an audit needs to enumerate, for every
5
+ known AI crawler, whether it has its own group and whether that group (or
6
+ the wildcard group) disallows the whole site.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ @dataclass
14
+ class Group:
15
+ agents: list[str] = field(default_factory=list)
16
+ disallow: list[str] = field(default_factory=list)
17
+ allow: list[str] = field(default_factory=list)
18
+
19
+
20
+ def parse_robots_txt(text: str) -> list[Group]:
21
+ groups: list[Group] = []
22
+ current: Group | None = None
23
+ seen_directive_since_agent = True # forces first "User-agent" to start a group
24
+
25
+ for raw_line in text.splitlines():
26
+ line = raw_line.split("#", 1)[0].strip()
27
+ if not line or ":" not in line:
28
+ continue
29
+ field_name, _, value = line.partition(":")
30
+ field_name = field_name.strip().lower()
31
+ value = value.strip()
32
+
33
+ if field_name == "user-agent":
34
+ if current is None or not seen_directive_since_agent:
35
+ current = Group()
36
+ groups.append(current)
37
+ current.agents.append(value)
38
+ seen_directive_since_agent = False
39
+ elif field_name == "disallow" and current is not None:
40
+ current.disallow.append(value)
41
+ seen_directive_since_agent = True
42
+ elif field_name == "allow" and current is not None:
43
+ current.allow.append(value)
44
+ seen_directive_since_agent = True
45
+ # sitemap / crawl-delay / other fields intentionally ignored here;
46
+ # sitemap presence is checked separately by audit.py.
47
+
48
+ return groups
49
+
50
+
51
+ def blocks_everything(group: Group) -> bool:
52
+ """True if this group disallows the whole site and doesn't carve out
53
+ an override Allow for it (Allow: / or Allow: with empty/root path)."""
54
+ has_blanket_disallow = any(d.strip() == "/" for d in group.disallow)
55
+ if not has_blanket_disallow:
56
+ return False
57
+ has_override_allow = any(a.strip() == "/" for a in group.allow)
58
+ return not has_override_allow
59
+
60
+
61
+ def agent_status(groups: list[Group], agent: str) -> str:
62
+ """Return 'blocked', 'allowed-explicit', or 'allowed-default' for one AI agent.
63
+
64
+ 'allowed-default' means the site has no group naming this agent and no
65
+ wildcard (*) group blocking everything — the crawler is free to fetch
66
+ under robots.txt's default-allow behaviour, but the site owner never
67
+ made an explicit decision either way.
68
+ """
69
+ agent_lower = agent.lower()
70
+ named_group = next(
71
+ (g for g in groups if any(a.lower() == agent_lower for a in g.agents)),
72
+ None,
73
+ )
74
+ if named_group is not None:
75
+ return "blocked" if blocks_everything(named_group) else "allowed-explicit"
76
+
77
+ wildcard_group = next((g for g in groups if "*" in g.agents), None)
78
+ if wildcard_group is not None and blocks_everything(wildcard_group):
79
+ return "blocked"
80
+ return "allowed-default"
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: aivisible-fix
3
+ Version: 0.2.0
4
+ Summary: Audit and auto-fix a website's visibility to AI crawlers and answer engines (GPTBot, ClaudeBot, PerplexityBot, and more).
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # aivisible-fix
12
+
13
+ Audits a website for the specific things that make it invisible to AI
14
+ crawlers and AI answer engines (ChatGPT, Claude, Perplexity, Gemini/AI
15
+ Overviews, and others) — and, unlike the free "checker" tools already on
16
+ the market, **generates the fix**, not just a score.
17
+
18
+ ```
19
+ $ aivisible-fix audit https://example.com
20
+ AI-visibility audit for https://example.com
21
+ Score: 40/100
22
+
23
+ [FAIL] robots_reachable: robots.txt missing or unreachable ...
24
+ [PASS] no_ai_crawlers_blocked: No known AI crawler is explicitly blocked.
25
+ [FAIL] llms_txt_present: No /llms.txt ...
26
+ ...
27
+
28
+ $ aivisible-fix fix https://example.com --name "Example Co" --description "..."
29
+ Wrote robots.fixed.txt
30
+ Wrote llms.txt
31
+ Review both files, then deploy them at your site root. Nothing was auto-published.
32
+ ```
33
+
34
+ ## What it checks
35
+
36
+ 1. **robots.txt reachable** — the baseline signal every crawler reads first.
37
+ 2. **No AI crawler is explicitly blocked** — checked per-agent (GPTBot,
38
+ ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-Web, anthropic-ai,
39
+ PerplexityBot, Perplexity-User, Google-Extended, CCBot, Bytespider,
40
+ Amazonbot, Applebot-Extended, cohere-ai, Meta-ExternalAgent, Diffbot,
41
+ YouBot — see `aivisible_fix/crawlers.py`), not just a blanket `Disallow: /`.
42
+ 3. **llms.txt present** — the emerging convention (llmstxt.org) for a short,
43
+ LLM-readable summary of what a site is and which pages matter.
44
+ 4. **sitemap.xml present and valid**, and **referenced from robots.txt**.
45
+ 5. **JSON-LD structured data on the homepage** — what answer engines lean on
46
+ to extract facts (org, product, FAQ, etc.) without guessing from prose.
47
+
48
+ Score is out of 100, weighted toward the two things that, per current
49
+ published research, cause the overwhelming majority of AI-search invisibility:
50
+ blocked crawlers (40 pts) and missing discovery trail — sitemap + llms.txt
51
+ (40 pts combined).
52
+
53
+ ## Why this instead of the free checkers
54
+
55
+ Search "AI visibility checker" and you'll find eight-plus free tools
56
+ (isvisible.ai, llmpulse.ai, siftly.ai, and others) that already do the
57
+ scoring for free — that part of the market is a commodity and not worth
58
+ competing on head to head.
59
+
60
+ None of them **write the fix**. This tool's differentiator is that
61
+ `aivisible-fix fix` outputs a ready-to-deploy patched `robots.txt` (every
62
+ existing line preserved, blocked AI agents explicitly re-allowed) and a
63
+ starter `llms.txt` — output, not just diagnosis. The natural extension
64
+ (not built yet — see ROADMAP below) is a GitHub App that opens this as a
65
+ pull request automatically, the way Dependabot does for dependency bumps,
66
+ plus scheduled re-audits so a clean report doesn't silently rot as new
67
+ crawlers ship.
68
+
69
+ ## Badge
70
+
71
+ ```
72
+ $ aivisible-fix badge https://example.com
73
+ Wrote aivisible-badge.svg (score 40/100). Host it yourself and embed:
74
+ <a href="https://example.com"><img src="/aivisible-badge.svg" alt="AI-visible: 40/100"></a>
75
+ ```
76
+
77
+ Static SVG, generated locally — no server or account required. Color tracks
78
+ score (red under 50, amber under 80, green at 80+).
79
+
80
+ ## Install
81
+
82
+ ```
83
+ pip install -e .
84
+ ```
85
+
86
+ No dependencies beyond the Python 3.10+ standard library — deliberately, so
87
+ it's trivial to also ship as a single-file GitHub Action.
88
+
89
+ ## Status
90
+
91
+ v0.1 — functional CLI, tested against live sites (see commit history / this
92
+ was built and smoke-tested against example.com and nytimes.com, correctly
93
+ detecting NYT's well-documented blanket block of AI crawlers). Not yet
94
+ published to PyPI or packaged as a GitHub Action — see BUSINESS_PLAN.md for
95
+ what's next and what needs a human to do it (PyPI account, GitHub App
96
+ registration, a payment processor for the paid tier).
@@ -0,0 +1,14 @@
1
+ aivisible_fix/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
2
+ aivisible_fix/audit.py,sha256=8zwcD6P6fF29fpNSqLMIIqnro9Dmyg1unaRH9nhyJFo,4169
3
+ aivisible_fix/badge.py,sha256=AV0SElvpNvs2L10vTwliPDJfL0K5pklLcKJdWflbvz0,1883
4
+ aivisible_fix/cli.py,sha256=3ilAUCNdWAXCwLTYRoJpo2DniBR74X-gPEhdE4xgJqE,3813
5
+ aivisible_fix/crawlers.py,sha256=liBonx36LEfuVannV_rjyZSuXSVM9XZ3Dn6u6HN3xB8,1588
6
+ aivisible_fix/fetch.py,sha256=NodkYB8MU7H80TW2BtKbVCP63V8Q7CyLmn6YFUis8cA,1337
7
+ aivisible_fix/fix.py,sha256=KgtjznOf2nzGSEPg_wXyTg_URsW0iaTzZDLk_h6vkYM,2406
8
+ aivisible_fix/robots.py,sha256=zDvoOS3bhAtlDnI-T__vHgIcj77Htk8UX3wi81Nh-fk,3106
9
+ aivisible_fix-0.2.0.dist-info/licenses/LICENSE,sha256=Y45HgrkRjIEDGPeRWopcdTdml5sdmCZldO4xAF83y_o,1083
10
+ aivisible_fix-0.2.0.dist-info/METADATA,sha256=Fw3ayjDznNg4DNPHf__Tc8ij0KHW7hrMBXzMBwcgnb0,3960
11
+ aivisible_fix-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
12
+ aivisible_fix-0.2.0.dist-info/entry_points.txt,sha256=HFhxAuWnV_5tmEZ2_f0-I4scFkRTVi7p1z_-VtIqACg,57
13
+ aivisible_fix-0.2.0.dist-info/top_level.txt,sha256=tK5JXbtjsne4mC_tNuVima9beg5B8OYwDaSKGCTVIxU,14
14
+ aivisible_fix-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aivisible-fix = aivisible_fix.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aivisible-fix contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ aivisible_fix