agentimization 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +39 -155
  2. package/dist/index.js +186 -167
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,193 +1,77 @@
1
- <p align="center">
2
- <img src="https://img.shields.io/npm/v/agentimization?style=flat-square&color=blue" alt="npm version" />
3
- <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license" />
4
- <img src="https://img.shields.io/badge/checks-35-purple?style=flat-square" alt="checks" />
5
- </p>
1
+ # agentimization
6
2
 
7
- <h1 align="center">agentimization</h1>
3
+ [![npm version](https://img.shields.io/npm/v/agentimization?style=flat-square&color=blue)](https://www.npmjs.com/package/agentimization)
8
4
 
9
- <p align="center">
10
- GEO audit for agent-ready websites.<br/>
11
- One command to check if AI agents can discover, parse, and cite your content.
12
- </p>
13
-
14
- ---
15
-
16
- ## Why
5
+ ```text
6
+ ╭───────────────────────────────────────────────╮
7
+ ▓░▒▓░░▒░▓▒░▓▓░▒░▓░░▒▓▒░▓░░▓▒░▓░▒░▓░░▒▓░░ │
8
+ │ ░▓▒░▓░░▒▓▒░▓░░▒▓▓░▒░▓▒░░▓▒░▓░▒░░▓▒░░▓░▒ │
9
+ │ ▓░▒▓░░▒▓▒░░▓░▒▓▒░░▓░░▓▒░▓░▒░░▓▒░▓░░▒▓░ │
10
+ │ ░▒▓░▒░▓▒░░▓░▒▓░░▒▓▒░░▓░▒▓░░▒▓░ agentimization │
11
+ ╰───────────────────────────────────────────────╯
12
+ ```
17
13
 
18
- AI agents (Claude, ChatGPT, Perplexity, Gemini) are becoming a major source of traffic and citations. But most websites are invisible to them — no `llms.txt`, no markdown endpoints, no structured data, client-rendered content that crawlers can't read.
14
+ geo audit for agent-ready websites and projects.
19
15
 
20
- **Agentimization** runs checks across 8 categories and gives you a GEO score from 0–100, with specific fixes you can hand off to an AI coding agent.
16
+ geomaxx your site so ai agents can actually find, parse, and cite it.
21
17
 
22
- ## Install
18
+ ## install
23
19
 
24
20
  ```bash
25
21
  npx agentimization https://your-site.com
26
22
  ```
27
23
 
28
- Or install globally:
29
-
30
- ```bash
31
- npm install -g agentimization
32
- ```
33
-
34
- ## Usage
24
+ ## usage
35
25
 
36
- ### Audit a live site
26
+ audit a live site:
37
27
 
38
28
  ```bash
39
- agentimization https://docs.anthropic.com
29
+ agentimization https://docs.your-site.com
40
30
  ```
41
31
 
42
- ```text
43
- ╭───────────────────────────────────────────────╮
44
- │ ▓░▒▓░░▒░▓▒░▓▓░▒░▓░░▒▓▒░▓░░▓▒░▓░▒░▓░░▒▓░░ │
45
- │ ░▓▒░▓░░▒▓▒░▓░░▒▓▓░▒░▓▒░░▓▒░▓░▒░░▓▒░░▓░▒ │
46
- │ ▓░▒▓░░▒▓▒░░▓░▒▓▒░░▓░░▓▒░▓░▒░░▓▒░▓░░▒▓░ │
47
- │ ░▒▓░▒░▓▒░░▓░▒▓░░▒▓▒░░▓░▒▓░░▒▓░ agentimization │
48
- │ │
49
- │ https://docs.anthropic.com │
50
- │ │
51
- │ Crawling the site, one sec… │
52
- ╰───────────────────────────────────────────────╯
53
- ```
54
-
55
- ### Audit a local directory (great for CI)
32
+ audit a local directory:
56
33
 
57
34
  ```bash
58
35
  agentimization .
59
- agentimization ./docs
60
36
  ```
61
37
 
62
- ### Output formats
38
+ pipe results to a tool or file:
63
39
 
64
40
  ```bash
65
- # JSON for CI pipelines
66
- agentimization https://example.com --json
67
-
68
- # Markdown report — paste into Claude, ChatGPT, etc.
69
- agentimization https://example.com --md
70
-
71
- # Filter by category
72
- agentimization https://example.com --category content-discoverability
41
+ agentimization https://your-site.com --json > report.json
42
+ agentimization https://your-site.com --md | pbcopy
73
43
  ```
74
44
 
75
- ### After the audit
76
-
77
- Agentimization shows an interactive menu when the audit finishes:
78
-
79
- - **Copy fix prompt to clipboard** — structured markdown an AI coding agent can use to fix your GEO issues
80
- - **Save JSON report** — full audit data written to `agentimization-report.json`
81
- - **Run another URL or path** — keep the session open and audit the next site
82
- - **Exit**
83
-
84
- ## Checks
85
-
86
- Agentimization runs **36 checks** across **8 categories**:
45
+ ## what it checks
87
46
 
88
- | Category | What it checks |
89
- |---|---|
90
- | **Content Discoverability** | `llms.txt` existence, structure, size, coverage, link resolution. Sitemap presence. `robots.txt` AI agent rules. |
91
- | **Markdown Availability** | `.md` URL support, `Accept: text/markdown` content negotiation, HTML↔markdown parity. |
92
- | **Content Structure** | Code fence validity, heading hierarchy, tabbed content serialization. |
93
- | **Page Size & Rendering** | SSR vs CSR detection, HTML/markdown page size, content start position (boilerplate ratio). |
94
- | **URL Stability** | HTTP status codes, redirect behavior, cache header hygiene. |
95
- | **Authentication & Access** | Auth gate detection, alternative access paths for gated content. |
96
- | **GEO Signals** | Structured data (JSON-LD), citation worthiness, topical authority, content freshness, E-E-A-T signals, FAQ schema, canonical URLs. |
97
- | **Agent Protocols** | AGENTS.md, MCP server card, API catalog (RFC 9727), content signals (AI usage declarations), Link headers (RFC 8288), agent skills index. |
47
+ 36 checks across 8 categories. each one is a thing ai agents need to discover, parse, or cite your content.
98
48
 
99
- ## Scoring
49
+ - content discoverability: `llms.txt`, sitemap, robots
50
+ - markdown availability: `.md` urls, content negotiation
51
+ - content structure: headings, code fences, hidden tabs
52
+ - page size and rendering: ssr vs csr, boilerplate ratio
53
+ - url stability: status codes, redirects, canonicals
54
+ - authentication and access: gates, alternative paths
55
+ - geo signals: json-ld, citations, freshness, e-e-a-t
56
+ - agent protocols: mcp card, api catalog, agents.md, link headers
100
57
 
101
- Each check returns **pass**, **warn**, **fail**, **skip**, or **info**. Checks are weighted by importance, and scores roll up into category scores and an overall grade:
58
+ ## how it works
102
59
 
103
- | Grade | Score |
104
- |---|---|
105
- | A+ | 95–100 |
106
- | A | 85–94 |
107
- | B | 70–84 |
108
- | C | 55–69 |
109
- | D | 40–54 |
110
- | F | 0–39 |
60
+ it samples up to 10 pages of your site, runs 36 checks against the html, headers, and well-known files, then weights them into a 0 to 100 score. failed checks come with a suggestion you can paste into your ai coding agent.
111
61
 
112
- ## Example scores
62
+ ## requirements
113
63
 
114
- How popular sites score on Agentimization (approximate, scores change as sites update):
64
+ node 18 or newer.
115
65
 
116
- | Site | Grade | Score | Notes |
117
- |---|---|---|---|
118
- | `docs.anthropic.com` | **A** | 88 | Strong `llms.txt`, good markdown, structured data |
119
- | `docs.stripe.com` | **A** | 91 | Excellent discoverability, markdown endpoints, great structure |
120
- | `nextjs.org/docs` | **B** | 76 | Good SSR, missing `llms.txt`, decent GEO signals |
121
- | `react.dev` | **B** | 72 | Good structure, no `llms.txt`, client-heavy rendering |
122
- | `en.wikipedia.org` | **A** | 86 | Great content structure, strong citations, no `llms.txt` |
123
- | `medium.com` | **D** | 45 | Auth gates, weak markdown, no `llms.txt` |
124
- | `substack.com` | **C** | 58 | Mixed access, some content gated |
125
-
126
- > These are illustrative examples. Run `agentimization <url>` to get real-time scores.
127
-
128
- ## Local mode
129
-
130
- When you pass a directory path instead of a URL, Agentimization runs in **local mode**:
131
-
132
- - Scans your files on disk (HTML, markdown, `llms.txt`, `robots.txt`, `sitemap.xml`)
133
- - Skips network-only checks (content negotiation, auth detection, cache headers, etc.)
134
- - Perfect as a **CI pre-deploy step** — catch GEO regressions before they ship
135
-
136
- ```bash
137
- # In CI
138
- agentimization . --json
139
- # Exit code 1 if score < 50
140
- ```
141
-
142
- ## Programmatic API
66
+ ## programmatic use
143
67
 
144
68
  ```typescript
145
- import { audit, auditLocal } from "@agentimization/core"
69
+ import { audit } from "@agentimization/core"
146
70
 
147
- // Remote audit
148
- const result = await audit("https://docs.anthropic.com")
71
+ const result = await audit("https://your-site.com")
149
72
  console.log(result.grade, result.overall_score)
150
-
151
- // Local audit
152
- const local = await auditLocal("./docs")
153
- console.log(local.grade, local.overall_score)
154
-
155
- // With options
156
- const result = await audit("https://example.com", {
157
- sampleSize: 20,
158
- categories: ["content-discoverability", "geo-signals"],
159
- onEvent: (event) => console.log(event),
160
- })
161
- ```
162
-
163
- ## What is GEO?
164
-
165
- **Generative Engine Optimization** is like SEO, but for AI. Instead of optimizing for Google's crawlers and ranking algorithm, GEO optimizes for AI agents that need to:
166
-
167
- 1. **Discover** your content (via `llms.txt`, sitemaps, `robots.txt`)
168
- 2. **Parse** it efficiently (markdown availability, clean HTML, SSR)
169
- 3. **Cite** it accurately (structured data, canonical URLs, E-E-A-T signals)
170
-
171
- Sites that score well on Agentimization are more likely to be surfaced and cited by Claude, ChatGPT, Perplexity, and other generative engines.
172
-
173
- ## Contributing
174
-
175
- ```bash
176
- git clone https://github.com/antlio/agentimization
177
- cd agentimization
178
- bun install
179
- bun run build
180
- bun run typecheck
181
- ```
182
-
183
- The monorepo structure:
184
-
185
- ```
186
- packages/shared — Types, schemas, constants
187
- packages/core — Audit engine + all 36 checks
188
- apps/cli — CLI (Commander.js + Ink)
189
73
  ```
190
74
 
191
- ## License
75
+ ## license
192
76
 
193
- MIT
77
+ mit
package/dist/index.js CHANGED
@@ -9,7 +9,6 @@ var __export = (target, all) => {
9
9
  import { Command } from "commander";
10
10
  import { render } from "ink";
11
11
  import React2 from "react";
12
- import { resolve as resolve3 } from "path";
13
12
 
14
13
  // ../../node_modules/zod/v3/external.js
15
14
  var external_exports = {};
@@ -4052,7 +4051,9 @@ var coerce = {
4052
4051
  };
4053
4052
  var NEVER = INVALID;
4054
4053
 
4055
- // node_modules/@agentimization/core/node_modules/@agentimization/shared/dist/types.js
4054
+ // node_modules/@agentimization/core/dist/index.js
4055
+ import { readFileSync, readdirSync, existsSync } from "fs";
4056
+ import { dirname, join, relative, extname } from "path";
4056
4057
  var CHECK_STATUSES = ["pass", "warn", "fail", "skip", "info"];
4057
4058
  var CHECK_CATEGORIES = [
4058
4059
  "content-discoverability",
@@ -4104,8 +4105,6 @@ var DEFAULT_CONFIG = {
4104
4105
  onEvent: () => {
4105
4106
  }
4106
4107
  };
4107
-
4108
- // node_modules/@agentimization/core/dist/checks/content-discoverability.js
4109
4108
  var llmsTxtExists = {
4110
4109
  id: "llms-txt-exists",
4111
4110
  name: "llms.txt Exists",
@@ -4151,17 +4150,13 @@ var llmsTxtValid = {
4151
4150
  const issues = [];
4152
4151
  const lines = ctx.llmsTxt.split("\n");
4153
4152
  const hasH1 = lines.some((l) => /^#\s+/.test(l));
4154
- if (!hasH1)
4155
- issues.push("Missing H1 title");
4153
+ if (!hasH1) issues.push("Missing H1 title");
4156
4154
  const hasBlockquote = lines.some((l) => /^>\s+/.test(l));
4157
- if (!hasBlockquote)
4158
- issues.push("Missing blockquote description");
4155
+ if (!hasBlockquote) issues.push("Missing blockquote description");
4159
4156
  const hasHeadingSections = lines.some((l) => /^##\s+/.test(l));
4160
- if (!hasHeadingSections)
4161
- issues.push("Missing ## section headings");
4157
+ if (!hasHeadingSections) issues.push("Missing ## section headings");
4162
4158
  const hasLinks = /\[.+\]\(.+\)/.test(ctx.llmsTxt);
4163
- if (!hasLinks)
4164
- issues.push("No markdown links found");
4159
+ if (!hasLinks) issues.push("No markdown links found");
4165
4160
  if (issues.length === 0) {
4166
4161
  return {
4167
4162
  id: "llms-txt-valid",
@@ -4245,8 +4240,7 @@ var llmsTxtFreshness = {
4245
4240
  };
4246
4241
  }
4247
4242
  const sharedRegistrable = (a, b) => {
4248
- if (a === b)
4249
- return true;
4243
+ if (a === b) return true;
4250
4244
  const aParts = a.split(".");
4251
4245
  const bParts = b.split(".");
4252
4246
  const tail = (parts) => parts.slice(-2).join(".");
@@ -4255,8 +4249,7 @@ var llmsTxtFreshness = {
4255
4249
  const keyFor = (raw) => {
4256
4250
  try {
4257
4251
  const u = new URL(raw, ctx.baseUrl.origin);
4258
- if (!sharedRegistrable(u.hostname, ctx.baseUrl.hostname))
4259
- return null;
4252
+ if (!sharedRegistrable(u.hostname, ctx.baseUrl.hostname)) return null;
4260
4253
  let path = u.pathname.length > 1 ? u.pathname.replace(/\/+$/, "") : u.pathname;
4261
4254
  path = path.replace(/\.(md|mdx|markdown)$/i, "");
4262
4255
  return path.toLowerCase();
@@ -4269,14 +4262,12 @@ var llmsTxtFreshness = {
4269
4262
  let match;
4270
4263
  while ((match = linkRegex.exec(ctx.llmsTxt)) !== null) {
4271
4264
  const k = keyFor(match[1]);
4272
- if (k)
4273
- llmsKeys.add(k);
4265
+ if (k) llmsKeys.add(k);
4274
4266
  }
4275
4267
  const sitemapKeys = /* @__PURE__ */ new Set();
4276
4268
  for (const u of ctx.sitemapUrls) {
4277
4269
  const k = keyFor(u);
4278
- if (k)
4279
- sitemapKeys.add(k);
4270
+ if (k) sitemapKeys.add(k);
4280
4271
  }
4281
4272
  if (sitemapKeys.size === 0 || llmsKeys.size === 0) {
4282
4273
  return {
@@ -4361,11 +4352,15 @@ var llmsTxtLinksResolve = {
4361
4352
  };
4362
4353
  }
4363
4354
  const sampled = urls.slice(0, 10);
4364
- const results = await Promise.allSettled(sampled.map(async (url) => {
4365
- const resp = await fetch(url, { method: "HEAD", redirect: "follow" });
4366
- return { url, status: resp.status };
4367
- }));
4368
- const resolved = results.filter((r) => r.status === "fulfilled" && r.value.status >= 200 && r.value.status < 400).length;
4355
+ const results = await Promise.allSettled(
4356
+ sampled.map(async (url) => {
4357
+ const resp = await fetch(url, { method: "HEAD", redirect: "follow" });
4358
+ return { url, status: resp.status };
4359
+ })
4360
+ );
4361
+ const resolved = results.filter(
4362
+ (r) => r.status === "fulfilled" && r.value.status >= 200 && r.value.status < 400
4363
+ ).length;
4369
4364
  if (resolved === sampled.length) {
4370
4365
  return {
4371
4366
  id: "llms-txt-links-resolve",
@@ -4527,9 +4522,10 @@ var robotsTxtAgentRules = {
4527
4522
  currentAgent = agentMatch[1].trim();
4528
4523
  continue;
4529
4524
  }
4530
- const matchedAgent = aiAgents.find((a) => currentAgent === a || currentAgent === "*");
4531
- if (!matchedAgent)
4532
- continue;
4525
+ const matchedAgent = aiAgents.find(
4526
+ (a) => currentAgent === a || currentAgent === "*"
4527
+ );
4528
+ if (!matchedAgent) continue;
4533
4529
  if (/^Disallow:\s*\/\s*$/i.test(line) && currentAgent !== "*") {
4534
4530
  blocked.push(currentAgent);
4535
4531
  } else if (/^Allow:\s*\//i.test(line) && currentAgent !== "*") {
@@ -4567,8 +4563,6 @@ var contentDiscoverabilityChecks = [
4567
4563
  sitemapExists,
4568
4564
  robotsTxtAgentRules
4569
4565
  ];
4570
-
4571
- // node_modules/@agentimization/core/dist/utils/fetch.js
4572
4566
  var makeHeaders = (config) => ({
4573
4567
  "User-Agent": config.userAgent ?? DEFAULT_CONFIG.userAgent,
4574
4568
  Accept: "text/html,application/xhtml+xml,text/markdown,text/plain,*/*"
@@ -4638,7 +4632,9 @@ var fetchMany = async (urls, config = {}) => {
4638
4632
  const results = [];
4639
4633
  for (let i = 0; i < urls.length; i += concurrency) {
4640
4634
  const chunk = urls.slice(i, i + concurrency);
4641
- const chunkResults = await Promise.allSettled(chunk.map((url) => fetchPage(url, config)));
4635
+ const chunkResults = await Promise.allSettled(
4636
+ chunk.map((url) => fetchPage(url, config))
4637
+ );
4642
4638
  for (const result of chunkResults) {
4643
4639
  if (result.status === "fulfilled") {
4644
4640
  results.push(result.value);
@@ -4647,8 +4643,6 @@ var fetchMany = async (urls, config = {}) => {
4647
4643
  }
4648
4644
  return results;
4649
4645
  };
4650
-
4651
- // node_modules/@agentimization/core/dist/checks/markdown-availability.js
4652
4646
  var markdownUrlSupport = {
4653
4647
  id: "markdown-url-support",
4654
4648
  name: "Markdown URL Support",
@@ -4766,8 +4760,7 @@ var markdownContentParity = {
4766
4760
  let totalMissing = 0;
4767
4761
  let checked = 0;
4768
4762
  for (const page of pagesWithMarkdown) {
4769
- if (!page.markdown)
4770
- continue;
4763
+ if (!page.markdown) continue;
4771
4764
  checked++;
4772
4765
  const htmlText = page.html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
4773
4766
  const htmlWords = new Set(htmlText.toLowerCase().split(/\s+/).filter((w) => w.length > 3));
@@ -4803,8 +4796,6 @@ var markdownAvailabilityChecks = [
4803
4796
  contentNegotiation,
4804
4797
  markdownContentParity
4805
4798
  ];
4806
-
4807
- // node_modules/@agentimization/core/dist/utils/html.js
4808
4799
  var stripHtml = (html) => html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
4809
4800
  var extractLinks = (html, baseUrl) => {
4810
4801
  const links = [];
@@ -4912,8 +4903,6 @@ var parseSitemapUrls = (xml) => {
4912
4903
  }
4913
4904
  return urls;
4914
4905
  };
4915
-
4916
- // node_modules/@agentimization/core/dist/checks/page-size.js
4917
4906
  var MAX_HTML_CHARS = 5e4;
4918
4907
  var MAX_MD_CHARS = 5e4;
4919
4908
  var renderingStrategy = {
@@ -4935,8 +4924,7 @@ var renderingStrategy = {
4935
4924
  }
4936
4925
  let ssrCount = 0;
4937
4926
  for (const page of pages) {
4938
- if (hasServerRenderedContent(page.html))
4939
- ssrCount++;
4927
+ if (hasServerRenderedContent(page.html)) ssrCount++;
4940
4928
  }
4941
4929
  if (ssrCount === pages.length) {
4942
4930
  return {
@@ -5049,7 +5037,7 @@ var contentStartPosition = {
5049
5037
  id: "content-start-position",
5050
5038
  name: "Content Start Position",
5051
5039
  category: "page-size",
5052
- description: "Checks if main content starts within the first 10% of the HTML",
5040
+ description: "Checks how soon main content starts relative to total HTML",
5053
5041
  weight: 0.5,
5054
5042
  run: async (ctx) => {
5055
5043
  const pages = ctx.sampledPages.slice(0, 10);
@@ -5066,15 +5054,16 @@ var contentStartPosition = {
5066
5054
  url: p.url,
5067
5055
  position: findContentStartPosition(p.html)
5068
5056
  }));
5069
- const earlyStart = positions.filter((p) => p.position <= 0.1);
5070
- const medianPct = Math.round(positions.map((p) => p.position).sort((a, b) => a - b)[Math.floor(positions.length / 2)] * 100);
5071
- if (earlyStart.length === pages.length) {
5057
+ const medianPct = Math.round(
5058
+ positions.map((p) => p.position).sort((a, b) => a - b)[Math.floor(positions.length / 2)] * 100
5059
+ );
5060
+ if (medianPct <= 30) {
5072
5061
  return {
5073
5062
  id: "content-start-position",
5074
5063
  name: "Content Start Position",
5075
5064
  category: "page-size",
5076
5065
  status: "pass",
5077
- message: `Content starts within first 10% on all ${pages.length} sampled pages (median ${medianPct}%)`,
5066
+ message: `content starts at ${medianPct}% of html (median over ${pages.length} pages)`,
5078
5067
  metadata: { medianPct }
5079
5068
  };
5080
5069
  }
@@ -5082,10 +5071,10 @@ var contentStartPosition = {
5082
5071
  id: "content-start-position",
5083
5072
  name: "Content Start Position",
5084
5073
  category: "page-size",
5085
- status: "warn",
5086
- message: `Content starts late on ${pages.length - earlyStart.length}/${pages.length} pages (median ${medianPct}%)`,
5087
- suggestion: "Move main content higher in the HTML. AI agents may waste context window tokens on navigation, headers, and boilerplate before reaching actual content.",
5088
- metadata: { medianPct, earlyStart: earlyStart.length }
5074
+ status: medianPct <= 50 ? "warn" : "fail",
5075
+ message: `content starts at ${medianPct}% of html (median over ${pages.length} pages)`,
5076
+ suggestion: "trim head metadata or move main content higher in the html so ai agents do not waste context tokens on boilerplate before reaching real content.",
5077
+ metadata: { medianPct }
5089
5078
  };
5090
5079
  }
5091
5080
  };
@@ -5095,8 +5084,6 @@ var pageSizeChecks = [
5095
5084
  pageSizeMarkdown,
5096
5085
  contentStartPosition
5097
5086
  ];
5098
-
5099
- // node_modules/@agentimization/core/dist/checks/content-structure.js
5100
5087
  var markdownCodeFenceValidity = {
5101
5088
  id: "markdown-code-fence-validity",
5102
5089
  name: "Markdown Code Fence Validity",
@@ -5111,8 +5098,7 @@ var markdownCodeFenceValidity = {
5111
5098
  for (const page of ctx.sampledPages.slice(0, 10)) {
5112
5099
  const codeBlockRegex = /<code[\s\S]*?<\/code>/gi;
5113
5100
  const matches = page.html.match(codeBlockRegex);
5114
- if (matches)
5115
- totalFences2 += matches.length;
5101
+ if (matches) totalFences2 += matches.length;
5116
5102
  }
5117
5103
  if (totalFences2 === 0) {
5118
5104
  return {
@@ -5178,8 +5164,7 @@ var sectionHeaderQuality = {
5178
5164
  const issues = [];
5179
5165
  for (const page of pages) {
5180
5166
  const headings = extractHeadings(page.html);
5181
- if (headings.length === 0)
5182
- continue;
5167
+ if (headings.length === 0) continue;
5183
5168
  let pageGood = true;
5184
5169
  const hasH1 = headings.some((h) => h.level === 1);
5185
5170
  if (!hasH1) {
@@ -5197,8 +5182,7 @@ var sectionHeaderQuality = {
5197
5182
  break;
5198
5183
  }
5199
5184
  }
5200
- if (pageGood)
5201
- goodPages++;
5185
+ if (pageGood) goodPages++;
5202
5186
  }
5203
5187
  if (goodPages === pages.length) {
5204
5188
  return {
@@ -5272,8 +5256,6 @@ var contentStructureChecks = [
5272
5256
  sectionHeaderQuality,
5273
5257
  tabbedContentSerialization
5274
5258
  ];
5275
-
5276
- // node_modules/@agentimization/core/dist/checks/url-stability.js
5277
5259
  var httpStatusCodes = {
5278
5260
  id: "http-status-codes",
5279
5261
  name: "HTTP Status Codes",
@@ -5290,8 +5272,7 @@ var httpStatusCodes = {
5290
5272
  for (const url of testUrls) {
5291
5273
  try {
5292
5274
  const resp = await fetch(url, { method: "GET", redirect: "follow" });
5293
- if (resp.status === 404)
5294
- proper404++;
5275
+ if (resp.status === 404) proper404++;
5295
5276
  } catch {
5296
5277
  }
5297
5278
  }
@@ -5390,8 +5371,6 @@ var urlStabilityChecks = [
5390
5371
  redirectBehavior,
5391
5372
  cacheHeaderHygiene
5392
5373
  ];
5393
-
5394
- // node_modules/@agentimization/core/dist/checks/authentication.js
5395
5374
  var authGateDetection = {
5396
5375
  id: "auth-gate-detection",
5397
5376
  name: "Auth Gate Detection",
@@ -5439,7 +5418,9 @@ var authAlternativeAccess = {
5439
5418
  requiresNetwork: true,
5440
5419
  run: async (ctx) => {
5441
5420
  const pages = ctx.sampledPages.slice(0, 10);
5442
- const gated = pages.filter((p) => p.statusCode === 401 || p.statusCode === 403);
5421
+ const gated = pages.filter(
5422
+ (p) => p.statusCode === 401 || p.statusCode === 403
5423
+ );
5443
5424
  if (gated.length === 0) {
5444
5425
  return {
5445
5426
  id: "auth-alternative-access",
@@ -5474,28 +5455,18 @@ var authenticationChecks = [
5474
5455
  authGateDetection,
5475
5456
  authAlternativeAccess
5476
5457
  ];
5477
-
5478
- // node_modules/@agentimization/core/dist/checks/geo-signals.js
5479
5458
  var detectFramework = (pages) => {
5480
5459
  for (const page of pages) {
5481
5460
  const xpb = (page.headers["x-powered-by"] ?? "").toLowerCase();
5482
- if (xpb.includes("next.js"))
5483
- return "next";
5484
- if (xpb.includes("nuxt"))
5485
- return "nuxt";
5461
+ if (xpb.includes("next.js")) return "next";
5462
+ if (xpb.includes("nuxt")) return "nuxt";
5486
5463
  const html = page.html;
5487
- if (/\/_next\/static\//.test(html) || /<script[^>]+id="__NEXT_DATA__"/.test(html))
5488
- return "next";
5489
- if (/__NUXT__\s*=/.test(html) || /\/_nuxt\//.test(html))
5490
- return "nuxt";
5491
- if (/data-sveltekit-/.test(html))
5492
- return "sveltekit";
5493
- if (/<meta\s+name="generator"\s+content="Astro/i.test(html))
5494
- return "astro";
5495
- if (/<meta\s+name="generator"\s+content="WordPress/i.test(html))
5496
- return "wordpress";
5497
- if (/\/build\/_assets\/.*\.js/.test(html) && /window\.__remixContext/.test(html))
5498
- return "remix";
5464
+ if (/\/_next\/static\//.test(html) || /<script[^>]+id="__NEXT_DATA__"/.test(html)) return "next";
5465
+ if (/__NUXT__\s*=/.test(html) || /\/_nuxt\//.test(html)) return "nuxt";
5466
+ if (/data-sveltekit-/.test(html)) return "sveltekit";
5467
+ if (/<meta\s+name="generator"\s+content="Astro/i.test(html)) return "astro";
5468
+ if (/<meta\s+name="generator"\s+content="WordPress/i.test(html)) return "wordpress";
5469
+ if (/\/build\/_assets\/.*\.js/.test(html) && /window\.__remixContext/.test(html)) return "remix";
5499
5470
  }
5500
5471
  return null;
5501
5472
  };
@@ -5508,8 +5479,7 @@ var FRAMEWORK_DOCS = {
5508
5479
  wordpress: "https://yoast.com/structured-data-with-schema-org-the-ultimate-guide/"
5509
5480
  };
5510
5481
  var frameworkHint = (fw) => {
5511
- if (fw && FRAMEWORK_DOCS[fw])
5512
- return ` See: ${FRAMEWORK_DOCS[fw]}`;
5482
+ if (fw && FRAMEWORK_DOCS[fw]) return ` See: ${FRAMEWORK_DOCS[fw]}`;
5513
5483
  return " See: https://schema.org/docs/gs.html";
5514
5484
  };
5515
5485
  var structuredDataCoverage = {
@@ -5528,8 +5498,7 @@ var structuredDataCoverage = {
5528
5498
  withStructuredData++;
5529
5499
  for (const item of jsonLd) {
5530
5500
  const type = item?.["@type"];
5531
- if (typeof type === "string")
5532
- types.push(type);
5501
+ if (typeof type === "string") types.push(type);
5533
5502
  }
5534
5503
  }
5535
5504
  }
@@ -5606,8 +5575,7 @@ var citationWorthiness = {
5606
5575
  signals.withLists++;
5607
5576
  pageCitable = true;
5608
5577
  }
5609
- if (pageCitable)
5610
- citablePages++;
5578
+ if (pageCitable) citablePages++;
5611
5579
  }
5612
5580
  if (citablePages >= pages.length * 0.7) {
5613
5581
  return {
@@ -5640,9 +5608,10 @@ var topicalAuthoritySignals = {
5640
5608
  const pages = ctx.sampledPages.slice(0, 10);
5641
5609
  let totalInternalLinks = 0;
5642
5610
  let pagesWithGoodLinking = 0;
5611
+ const resolveBase = ctx.mode === "local" ? ctx.baseUrl.href : ctx.baseUrl.origin;
5643
5612
  for (const page of pages) {
5644
- const links = extractLinks(page.html, ctx.baseUrl.origin);
5645
- const internalLinks = ctx.mode === "local" ? links.filter((l) => !l.startsWith("http://") && !l.startsWith("https://")) : links.filter((l) => {
5613
+ const links = extractLinks(page.html, resolveBase);
5614
+ const internalLinks = ctx.mode === "local" ? links.filter((l) => l.startsWith("file:")) : links.filter((l) => {
5646
5615
  try {
5647
5616
  return new URL(l).origin === ctx.baseUrl.origin;
5648
5617
  } catch {
@@ -5650,8 +5619,7 @@ var topicalAuthoritySignals = {
5650
5619
  }
5651
5620
  });
5652
5621
  totalInternalLinks += internalLinks.length;
5653
- if (internalLinks.length >= 3)
5654
- pagesWithGoodLinking++;
5622
+ if (internalLinks.length >= 3) pagesWithGoodLinking++;
5655
5623
  }
5656
5624
  const avgLinks = pages.length > 0 ? Math.round(totalInternalLinks / pages.length) : 0;
5657
5625
  if (avgLinks >= 5 && pagesWithGoodLinking >= pages.length * 0.7) {
@@ -5737,12 +5705,11 @@ var eeatSignals = {
5737
5705
  return !!obj?.author;
5738
5706
  });
5739
5707
  const hasAuthorHtml = /class=["'][^"']*author[^"']*["']|rel=["']author["']/i.test(page.html);
5740
- if (hasAuthorMeta || hasAuthorJsonLd || hasAuthorHtml)
5741
- withAuthor++;
5708
+ if (hasAuthorMeta || hasAuthorJsonLd || hasAuthorHtml) withAuthor++;
5742
5709
  const hasCredentials = /Ph\.?D|M\.?D|CPA|certified|licensed|expert|specialist/i.test(page.html);
5743
- const hasAboutPage = extractLinks(page.html, ctx.baseUrl.origin).some((l) => /about|team|author/i.test(l));
5744
- if (hasCredentials || hasAboutPage)
5745
- withExpertise++;
5710
+ const linkBase = ctx.mode === "local" ? ctx.baseUrl.href : ctx.baseUrl.origin;
5711
+ const hasAboutPage = extractLinks(page.html, linkBase).some((l) => /about|team|author/i.test(l));
5712
+ if (hasCredentials || hasAboutPage) withExpertise++;
5746
5713
  }
5747
5714
  const score = (withAuthor + withExpertise) / (pages.length * 2);
5748
5715
  if (score >= 0.6) {
@@ -5782,8 +5749,7 @@ var faqSchema = {
5782
5749
  return type === "FAQPage" || type === "QAPage";
5783
5750
  });
5784
5751
  const hasFaqHtml = /<details|<summary|class=["'][^"']*faq[^"']*["']|id=["'][^"']*faq[^"']*["']/i.test(page.html);
5785
- if (hasFaqSchema || hasFaqHtml)
5786
- withFaq++;
5752
+ if (hasFaqSchema || hasFaqHtml) withFaq++;
5787
5753
  }
5788
5754
  if (withFaq > 0) {
5789
5755
  return {
@@ -5813,6 +5779,15 @@ var canonicalUrlConsistency = {
5813
5779
  description: "Checks if pages have consistent canonical URLs",
5814
5780
  weight: 0.5,
5815
5781
  run: async (ctx) => {
5782
+ if (ctx.mode === "local") {
5783
+ return {
5784
+ id: "canonical-url-consistency",
5785
+ name: "Canonical URL Consistency",
5786
+ category: "geo-signals",
5787
+ status: "info",
5788
+ message: "only meaningful for live urls. re-run against a deployed site to verify"
5789
+ };
5790
+ }
5816
5791
  const pages = ctx.sampledPages.slice(0, 10);
5817
5792
  let withCanonical = 0;
5818
5793
  let selfReferencing = 0;
@@ -5869,8 +5844,6 @@ var geoSignalChecks = [
5869
5844
  faqSchema,
5870
5845
  canonicalUrlConsistency
5871
5846
  ];
5872
-
5873
- // node_modules/@agentimization/core/dist/checks/agent-protocols.js
5874
5847
  var mcpServerCard = {
5875
5848
  id: "mcp-server-card",
5876
5849
  name: "MCP Server Card",
@@ -6004,12 +5977,9 @@ var contentSignals = {
6004
5977
  }
6005
5978
  const text = ctx.robotsTxt.toLowerCase();
6006
5979
  const signals = [];
6007
- if (/ai[-_]?train/i.test(ctx.robotsTxt))
6008
- signals.push("ai-train");
6009
- if (/ai[-_]?input/i.test(ctx.robotsTxt))
6010
- signals.push("ai-input");
6011
- if (/content[-_]?signals?/i.test(ctx.robotsTxt))
6012
- signals.push("content-signals");
5980
+ if (/ai[-_]?train/i.test(ctx.robotsTxt)) signals.push("ai-train");
5981
+ if (/ai[-_]?input/i.test(ctx.robotsTxt)) signals.push("ai-input");
5982
+ if (/content[-_]?signals?/i.test(ctx.robotsTxt)) signals.push("content-signals");
6013
5983
  const hasGPTBot = text.includes("gptbot");
6014
5984
  const hasClaudeBot = text.includes("claudebot") || text.includes("claude-web");
6015
5985
  const hasGoogleExtended = text.includes("google-extended");
@@ -6244,8 +6214,6 @@ var agentProtocolChecks = [
6244
6214
  agentSkillsIndex,
6245
6215
  agentsMd
6246
6216
  ];
6247
-
6248
- // node_modules/@agentimization/core/dist/checks/index.js
6249
6217
  var ALL_CHECKS = [
6250
6218
  ...contentDiscoverabilityChecks,
6251
6219
  ...markdownAvailabilityChecks,
@@ -6256,13 +6224,31 @@ var ALL_CHECKS = [
6256
6224
  ...geoSignalChecks,
6257
6225
  ...agentProtocolChecks
6258
6226
  ];
6259
-
6260
- // node_modules/@agentimization/core/dist/utils/local.js
6261
- import { readFileSync, readdirSync, existsSync } from "fs";
6262
- import { join, relative, extname } from "path";
6227
+ var readIfExists = (path) => {
6228
+ try {
6229
+ if (existsSync(path)) {
6230
+ return readFileSync(path, "utf-8");
6231
+ }
6232
+ } catch {
6233
+ }
6234
+ return void 0;
6235
+ };
6236
+ var findUpward = (start, names, maxDepth = 6) => {
6237
+ let current = start;
6238
+ for (let i = 0; i < maxDepth; i++) {
6239
+ for (const name of names) {
6240
+ const candidate = join(current, name);
6241
+ const value = readIfExists(candidate);
6242
+ if (value !== void 0) return value;
6243
+ }
6244
+ const parent = dirname(current);
6245
+ if (parent === current) break;
6246
+ current = parent;
6247
+ }
6248
+ return void 0;
6249
+ };
6263
6250
  var walkDir = (dir, extensions, maxDepth = 10) => {
6264
- if (maxDepth <= 0)
6265
- return [];
6251
+ if (maxDepth <= 0) return [];
6266
6252
  const results = [];
6267
6253
  try {
6268
6254
  const entries = readdirSync(dir, { withFileTypes: true });
@@ -6281,15 +6267,6 @@ var walkDir = (dir, extensions, maxDepth = 10) => {
6281
6267
  }
6282
6268
  return results;
6283
6269
  };
6284
- var readIfExists = (path) => {
6285
- try {
6286
- if (existsSync(path)) {
6287
- return readFileSync(path, "utf-8");
6288
- }
6289
- } catch {
6290
- }
6291
- return void 0;
6292
- };
6293
6270
  var buildLocalContext = (dirPath, config) => {
6294
6271
  const baseUrl = new URL(`file://${dirPath}`);
6295
6272
  const robotsTxt = readIfExists(join(dirPath, "robots.txt"));
@@ -6299,7 +6276,7 @@ var buildLocalContext = (dirPath, config) => {
6299
6276
  const mcpServerCard2 = readIfExists(join(dirPath, ".well-known", "mcp", "server-card.json"));
6300
6277
  const apiCatalog2 = readIfExists(join(dirPath, ".well-known", "api-catalog"));
6301
6278
  const agentSkillsIndex2 = readIfExists(join(dirPath, ".well-known", "agent-skills", "index.json"));
6302
- const agentsMd2 = readIfExists(join(dirPath, "AGENTS.md")) ?? readIfExists(join(dirPath, "AGENT.md"));
6279
+ const agentsMd2 = findUpward(dirPath, ["AGENTS.md", "AGENT.md"]);
6303
6280
  const sitemapUrls = sitemapXml ? parseSitemapUrls(sitemapXml) : [];
6304
6281
  if (!sitemapXml && robotsTxt) {
6305
6282
  const sitemapMatch = robotsTxt.match(/Sitemap:\s*(.+)/i);
@@ -6316,8 +6293,8 @@ var buildLocalContext = (dirPath, config) => {
6316
6293
  }
6317
6294
  const htmlFiles = walkDir(dirPath, /* @__PURE__ */ new Set([".html", ".htm"]));
6318
6295
  const mdFiles = walkDir(dirPath, /* @__PURE__ */ new Set([".md", ".mdx"]));
6319
- const allFiles = [...htmlFiles, ...mdFiles];
6320
- const sampled = allFiles.slice(0, config.sampleSize);
6296
+ const sampleSource = htmlFiles.length > 0 ? htmlFiles : mdFiles;
6297
+ const sampled = sampleSource.slice(0, config.sampleSize);
6321
6298
  const sampledPages = sampled.map((filePath) => {
6322
6299
  const content = readFileSync(filePath, "utf-8");
6323
6300
  const relPath = relative(dirPath, filePath);
@@ -6368,19 +6345,12 @@ var wrapMarkdownAsHtml = (md, title) => {
6368
6345
  html = html.replace(/^([^<\n].+)$/gm, "<p>$1</p>");
6369
6346
  return `<!DOCTYPE html><html><head><title>${title}</title></head><body><main>${html}</main></body></html>`;
6370
6347
  };
6371
-
6372
- // node_modules/@agentimization/core/dist/index.js
6373
6348
  var computeGrade = (score) => {
6374
- if (score >= 95)
6375
- return "A+";
6376
- if (score >= 85)
6377
- return "A";
6378
- if (score >= 70)
6379
- return "B";
6380
- if (score >= 55)
6381
- return "C";
6382
- if (score >= 40)
6383
- return "D";
6349
+ if (score >= 95) return "A+";
6350
+ if (score >= 85) return "A";
6351
+ if (score >= 70) return "B";
6352
+ if (score >= 55) return "C";
6353
+ if (score >= 40) return "D";
6384
6354
  return "F";
6385
6355
  };
6386
6356
  var buildRemoteContext = async (targetUrl, config) => {
@@ -6389,7 +6359,15 @@ var buildRemoteContext = async (targetUrl, config) => {
6389
6359
  emit({ type: "phase", phase: "fetching" });
6390
6360
  const baseUrl = new URL(targetUrl);
6391
6361
  const origin = baseUrl.origin;
6392
- const [robotsResult, llmsResult, llmsFullResult, sitemapResult, mcpCardResult, apiCatalogResult, agentSkillsResult] = await Promise.allSettled([
6362
+ const [
6363
+ robotsResult,
6364
+ llmsResult,
6365
+ llmsFullResult,
6366
+ sitemapResult,
6367
+ mcpCardResult,
6368
+ apiCatalogResult,
6369
+ agentSkillsResult
6370
+ ] = await Promise.allSettled([
6393
6371
  fetchText(`${origin}/robots.txt`, config),
6394
6372
  fetchText(`${origin}/llms.txt`, config),
6395
6373
  fetchText(`${origin}/llms-full.txt`, config),
@@ -6523,7 +6501,9 @@ var runAudit = async (ctx, config, start) => {
6523
6501
  overallScore = 50;
6524
6502
  }
6525
6503
  const categories = {};
6526
- const activeCategories = config.categories.filter((cat) => results.some((r) => r.category === cat));
6504
+ const activeCategories = config.categories.filter(
6505
+ (cat) => results.some((r) => r.category === cat)
6506
+ );
6527
6507
  for (const cat of activeCategories) {
6528
6508
  const catResults = results.filter((r) => r.category === cat);
6529
6509
  const catScorable = catResults.filter((r) => r.status !== "skip" && r.status !== "info");
@@ -7230,11 +7210,11 @@ var copyToClipboard = async (text) => {
7230
7210
  // src/ui/action-menu.tsx
7231
7211
  import { writeFileSync } from "fs";
7232
7212
  import { resolve } from "path";
7233
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7213
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7234
7214
  var FEEDBACK_OK = "oklch(0.858 0.109 142.7)";
7235
7215
  var FEEDBACK_ERR = "oklch(0.718 0.181 10.0)";
7236
- var MENU_OPTIONS = [
7237
- { label: "Copy fix prompt to clipboard", value: "copy", hint: "paste into Claude, ChatGPT, etc." },
7216
+ var buildMenuOptions = (hasIssues) => [
7217
+ ...hasIssues ? [{ label: "Copy fix prompt to clipboard", value: "copy", hint: "paste into Claude, ChatGPT, etc." }] : [],
7238
7218
  { label: "Save JSON report", value: "json", hint: "agentimization-report.json" },
7239
7219
  { label: "Run another URL or path", value: "retry" },
7240
7220
  { label: "Exit", value: "exit" }
@@ -7251,6 +7231,8 @@ var ActionMenu = ({
7251
7231
  const [feedback, setFeedback] = useState2(null);
7252
7232
  const [input, setInput] = useState2("");
7253
7233
  const [done, setDone] = useState2(false);
7234
+ const hasIssues = result.summary.failed > 0 || result.summary.warned > 0;
7235
+ const menuOptions = buildMenuOptions(hasIssues);
7254
7236
  const handleAction = (action) => {
7255
7237
  const promptOpts = { mode: isLocal ? "local" : "remote", target };
7256
7238
  switch (action) {
@@ -7285,6 +7267,7 @@ var ActionMenu = ({
7285
7267
  setDone(true);
7286
7268
  setTimeout(() => {
7287
7269
  exit();
7270
+ process.stdout.write("\n");
7288
7271
  if (result.overall_score < 50) process.exit(1);
7289
7272
  }, 50);
7290
7273
  break;
@@ -7297,13 +7280,16 @@ var ActionMenu = ({
7297
7280
  if (key.upArrow) {
7298
7281
  setSelected((prev) => Math.max(0, prev - 1));
7299
7282
  } else if (key.downArrow) {
7300
- setSelected((prev) => Math.min(MENU_OPTIONS.length - 1, prev + 1));
7283
+ setSelected((prev) => Math.min(menuOptions.length - 1, prev + 1));
7301
7284
  } else if (key.return) {
7302
- const option = MENU_OPTIONS[selected];
7285
+ const option = menuOptions[selected];
7303
7286
  handleAction(option.value);
7304
7287
  } else if (char === "q" || key.escape) {
7305
7288
  setDone(true);
7306
- setTimeout(() => exit(), 50);
7289
+ setTimeout(() => {
7290
+ exit();
7291
+ process.stdout.write("\n");
7292
+ }, 50);
7307
7293
  }
7308
7294
  return;
7309
7295
  }
@@ -7337,7 +7323,7 @@ var ActionMenu = ({
7337
7323
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
7338
7324
  /* @__PURE__ */ jsx4(Text4, { color: toInkColor(TEXT_DIM), children: " \u2500\u2500\u2500 What next? \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
7339
7325
  /* @__PURE__ */ jsx4(Text4, { color: toInkColor(TEXT_DIM), children: " Use \u2191\u2193 arrows, enter to select, q to quit" }),
7340
- /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: MENU_OPTIONS.map((opt, i) => {
7326
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: menuOptions.map((opt, i) => {
7341
7327
  const isSelected = i === selected;
7342
7328
  return /* @__PURE__ */ jsxs4(Box4, { paddingLeft: 2, children: [
7343
7329
  /* @__PURE__ */ jsxs4(Text4, { color: isSelected ? toInkColor(ACCENT_BLUE) : void 0, children: [
@@ -7352,7 +7338,10 @@ var ActionMenu = ({
7352
7338
  ] }) : null
7353
7339
  ] }, opt.value);
7354
7340
  }) }),
7355
- feedback ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { color: toInkColor(feedback.startsWith("\u2713") ? FEEDBACK_OK : FEEDBACK_ERR), children: feedback }) }) : null
7341
+ feedback ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
7342
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { color: toInkColor(feedback.startsWith("\u2713") ? FEEDBACK_OK : FEEDBACK_ERR), children: feedback }) }),
7343
+ /* @__PURE__ */ jsx4(Box4, { height: 1, children: /* @__PURE__ */ jsx4(Text4, { children: " " }) })
7344
+ ] }) : null
7356
7345
  ] });
7357
7346
  };
7358
7347
 
@@ -7433,7 +7422,7 @@ var ErrorActions = ({ onRetry }) => {
7433
7422
 
7434
7423
  // src/ui/target.ts
7435
7424
  import { existsSync as existsSync2, statSync } from "fs";
7436
- import { resolve as resolve2 } from "path";
7425
+ import { join as join2, resolve as resolve2 } from "path";
7437
7426
  var isLocalPath = (arg) => {
7438
7427
  if (arg === "." || arg === ".." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/")) {
7439
7428
  return true;
@@ -7449,11 +7438,30 @@ var normalizeUrl = (arg) => {
7449
7438
  const parsed = new URL(arg.startsWith("http") ? arg : `https://${arg}`);
7450
7439
  return parsed.href;
7451
7440
  };
7441
+ var BUILD_DIR_NAMES = ["dist", "build", "out", "_site", "public"];
7442
+ var hasIndex = (dir) => ["index.html", "index.htm", "index.md"].some((name) => existsSync2(join2(dir, name)));
7443
+ var findBuildDir = (root) => {
7444
+ for (const name of BUILD_DIR_NAMES) {
7445
+ const candidate = join2(root, name);
7446
+ if (existsSync2(candidate) && statSync(candidate).isDirectory() && hasIndex(candidate)) {
7447
+ return candidate;
7448
+ }
7449
+ }
7450
+ return void 0;
7451
+ };
7452
7452
  var resolveTarget = (input) => {
7453
7453
  const trimmed = input.trim();
7454
7454
  if (!trimmed) return { error: "Please enter a URL or path." };
7455
7455
  if (isLocalPath(trimmed)) {
7456
- return { target: resolve2(trimmed), isLocal: true };
7456
+ const requested = resolve2(trimmed);
7457
+ if (hasIndex(requested)) {
7458
+ return { target: requested, isLocal: true };
7459
+ }
7460
+ const buildDir = findBuildDir(requested);
7461
+ if (buildDir) {
7462
+ return { target: buildDir, isLocal: true, autoDetectedFrom: requested };
7463
+ }
7464
+ return { target: requested, isLocal: true };
7457
7465
  }
7458
7466
  try {
7459
7467
  return { target: normalizeUrl(trimmed), isLocal: false };
@@ -7464,7 +7472,13 @@ var resolveTarget = (input) => {
7464
7472
 
7465
7473
  // src/ui/app.tsx
7466
7474
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
7467
- var App = ({ target: initialTarget, isLocal: initialIsLocal, categories, sampleSize }) => {
7475
+ var App = ({
7476
+ target: initialTarget,
7477
+ isLocal: initialIsLocal,
7478
+ categories,
7479
+ sampleSize,
7480
+ autoDetectedFrom
7481
+ }) => {
7468
7482
  const [target, setTarget] = useState4(initialTarget);
7469
7483
  const [isLocal, setIsLocal] = useState4(initialIsLocal);
7470
7484
  const [phase, setPhase] = useState4("init");
@@ -7573,6 +7587,11 @@ var App = ({ target: initialTarget, isLocal: initialIsLocal, categories, sampleS
7573
7587
  gradeColor: result ? GRADE_COLORS[result.grade] : void 0
7574
7588
  }
7575
7589
  ) }),
7590
+ autoDetectedFrom ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
7591
+ "auto-detected build output (was ",
7592
+ autoDetectedFrom,
7593
+ ")"
7594
+ ] }) : null,
7576
7595
  isLocal && networkSkipped > 0 && phase === "done" ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
7577
7596
  networkSkipped,
7578
7597
  " network-only checks skipped"
@@ -7636,10 +7655,18 @@ Examples:
7636
7655
  $ agentimization ./docs --category content-structure
7637
7656
  $ agentimization ./public --md | pbcopy`).action(async (target, opts) => {
7638
7657
  const categories = opts.category ? [opts.category] : void 0;
7639
- const isLocal = isLocalPath(target);
7658
+ const resolved = resolveTarget(target);
7659
+ if ("error" in resolved) {
7660
+ console.error(`Error: ${resolved.error}`);
7661
+ return process.exit(1);
7662
+ }
7663
+ const { target: targetResolved, isLocal, autoDetectedFrom } = resolved;
7664
+ if (autoDetectedFrom && !opts.json) {
7665
+ console.error(`auditing built output at ${targetResolved}`);
7666
+ }
7640
7667
  if (opts.json) {
7641
7668
  try {
7642
- const result = isLocal ? await auditLocal(resolve3(target), { categories, sampleSize: opts.sampleSize }) : await audit(normalizeUrl2(target), { categories, sampleSize: opts.sampleSize });
7669
+ const result = isLocal ? await auditLocal(targetResolved, { categories, sampleSize: opts.sampleSize }) : await audit(targetResolved, { categories, sampleSize: opts.sampleSize });
7643
7670
  console.log(JSON.stringify(result, null, 2));
7644
7671
  if (result.overall_score < 50) process.exit(1);
7645
7672
  } catch (error) {
@@ -7651,8 +7678,8 @@ Examples:
7651
7678
  }
7652
7679
  if (opts.md) {
7653
7680
  try {
7654
- const result = isLocal ? await auditLocal(resolve3(target), { categories, sampleSize: opts.sampleSize }) : await audit(normalizeUrl2(target), { categories, sampleSize: opts.sampleSize });
7655
- console.log(generateAgentPrompt(result, { mode: isLocal ? "local" : "remote", target }));
7681
+ const result = isLocal ? await auditLocal(targetResolved, { categories, sampleSize: opts.sampleSize }) : await audit(targetResolved, { categories, sampleSize: opts.sampleSize });
7682
+ console.log(generateAgentPrompt(result, { mode: isLocal ? "local" : "remote", target: targetResolved }));
7656
7683
  if (result.overall_score < 50) process.exit(1);
7657
7684
  } catch (error) {
7658
7685
  const msg = error instanceof Error ? error.message : "Unknown error";
@@ -7661,24 +7688,16 @@ Examples:
7661
7688
  }
7662
7689
  return;
7663
7690
  }
7664
- const targetResolved = isLocal ? resolve3(target) : normalizeUrl2(target);
7665
7691
  render(
7666
7692
  React2.createElement(App, {
7667
7693
  target: targetResolved,
7668
7694
  isLocal,
7669
7695
  categories,
7670
- sampleSize: opts.sampleSize
7696
+ sampleSize: opts.sampleSize,
7697
+ autoDetectedFrom
7671
7698
  })
7672
7699
  );
7673
7700
  });
7674
- var normalizeUrl2 = (arg) => {
7675
- try {
7676
- return normalizeUrl(arg);
7677
- } catch {
7678
- console.error(`Error: Invalid URL "${arg}"`);
7679
- process.exit(1);
7680
- }
7681
- };
7682
7701
  try {
7683
7702
  program.parse();
7684
7703
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentimization",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "GEO audit CLI — check if your website is agent-ready",
5
5
  "license": "MIT",
6
6
  "author": "Anthony Lio <hello@antl.io>",
@@ -40,7 +40,7 @@
40
40
  "dev": "tsup --watch",
41
41
  "typecheck": "tsc --noEmit",
42
42
  "test": "vitest run",
43
- "prepublishOnly": "bun run build && cp ../../README.md ./README.md && cp ../../LICENSE ./LICENSE"
43
+ "prepublishOnly": "bun run build && cp -f ../../README.md ./README.md && cp -f ../../LICENSE ./LICENSE"
44
44
  },
45
45
  "dependencies": {
46
46
  "commander": "^14.0.3",