agentimization 0.1.0 → 0.1.2

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 +531 -171
  3. package/package.json +2 -2
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 = [];
@@ -4824,11 +4815,11 @@ var extractMetaTags = (html) => {
4824
4815
  const metaRegex = /<meta[^>]+(?:name|property)=["']([^"']+)["'][^>]+content=["']([^"']+)["']/gi;
4825
4816
  let match;
4826
4817
  while ((match = metaRegex.exec(html)) !== null) {
4827
- meta[match[1]] = match[2];
4818
+ meta[match[1].toLowerCase()] = match[2];
4828
4819
  }
4829
4820
  const metaRegex2 = /<meta[^>]+content=["']([^"']+)["'][^>]+(?:name|property)=["']([^"']+)["']/gi;
4830
4821
  while ((match = metaRegex2.exec(html)) !== null) {
4831
- meta[match[2]] = match[1];
4822
+ meta[match[2].toLowerCase()] = match[1];
4832
4823
  }
4833
4824
  return meta;
4834
4825
  };
@@ -4844,6 +4835,24 @@ var extractJsonLd = (html) => {
4844
4835
  }
4845
4836
  return results;
4846
4837
  };
4838
+ var readAttr = (attrs, name) => {
4839
+ const re = new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)')`, "i");
4840
+ const m = attrs.match(re);
4841
+ if (!m) return void 0;
4842
+ return m[1] ?? m[2];
4843
+ };
4844
+ var extractImages = (html) => {
4845
+ const images = [];
4846
+ const imgRegex = /<img\b([^>]*)>/gi;
4847
+ let match;
4848
+ while ((match = imgRegex.exec(html)) !== null) {
4849
+ const attrs = match[1];
4850
+ const src = readAttr(attrs, "src");
4851
+ if (src === void 0) continue;
4852
+ images.push({ src, alt: readAttr(attrs, "alt") });
4853
+ }
4854
+ return images;
4855
+ };
4847
4856
  var extractHeadings = (html) => {
4848
4857
  const headings = [];
4849
4858
  const regex = /<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi;
@@ -4912,8 +4921,6 @@ var parseSitemapUrls = (xml) => {
4912
4921
  }
4913
4922
  return urls;
4914
4923
  };
4915
-
4916
- // node_modules/@agentimization/core/dist/checks/page-size.js
4917
4924
  var MAX_HTML_CHARS = 5e4;
4918
4925
  var MAX_MD_CHARS = 5e4;
4919
4926
  var renderingStrategy = {
@@ -4935,8 +4942,7 @@ var renderingStrategy = {
4935
4942
  }
4936
4943
  let ssrCount = 0;
4937
4944
  for (const page of pages) {
4938
- if (hasServerRenderedContent(page.html))
4939
- ssrCount++;
4945
+ if (hasServerRenderedContent(page.html)) ssrCount++;
4940
4946
  }
4941
4947
  if (ssrCount === pages.length) {
4942
4948
  return {
@@ -4958,6 +4964,53 @@ var renderingStrategy = {
4958
4964
  };
4959
4965
  }
4960
4966
  };
4967
+ var substantialTextContent = {
4968
+ id: "substantial-text-content",
4969
+ name: "Substantial Text Content",
4970
+ category: "page-size",
4971
+ description: "Checks for at least 100 words of readable body text",
4972
+ weight: 0.8,
4973
+ run: async (ctx) => {
4974
+ const pages = ctx.sampledPages.slice(0, 10);
4975
+ if (pages.length === 0) {
4976
+ return {
4977
+ id: "substantial-text-content",
4978
+ name: "Substantial Text Content",
4979
+ category: "page-size",
4980
+ status: "skip",
4981
+ message: "No pages sampled"
4982
+ };
4983
+ }
4984
+ let withSubstantialContent = 0;
4985
+ let totalWords = 0;
4986
+ for (const page of pages) {
4987
+ const text = stripHtml(page.html);
4988
+ const words = text.split(/\s+/).filter((w) => w.length > 0).length;
4989
+ totalWords += words;
4990
+ if (words >= 100) withSubstantialContent++;
4991
+ }
4992
+ const avgWords = Math.round(totalWords / pages.length);
4993
+ if (withSubstantialContent === pages.length) {
4994
+ return {
4995
+ id: "substantial-text-content",
4996
+ name: "Substantial Text Content",
4997
+ category: "page-size",
4998
+ status: "pass",
4999
+ message: `All ${pages.length} pages have \u2265100 words of body text (avg ${avgWords})`,
5000
+ metadata: { withSubstantialContent, avgWords }
5001
+ };
5002
+ }
5003
+ return {
5004
+ id: "substantial-text-content",
5005
+ name: "Substantial Text Content",
5006
+ category: "page-size",
5007
+ status: withSubstantialContent > 0 ? "warn" : "fail",
5008
+ message: `${withSubstantialContent}/${pages.length} pages have \u2265100 words of body text (avg ${avgWords})`,
5009
+ suggestion: "Generative engines can't cite pages that are mostly images or short copy. Add at least 100 words of substantive text content per page.",
5010
+ metadata: { withSubstantialContent, avgWords }
5011
+ };
5012
+ }
5013
+ };
4961
5014
  var pageSizeHtml = {
4962
5015
  id: "page-size-html",
4963
5016
  name: "Page Size (HTML)",
@@ -5049,7 +5102,7 @@ var contentStartPosition = {
5049
5102
  id: "content-start-position",
5050
5103
  name: "Content Start Position",
5051
5104
  category: "page-size",
5052
- description: "Checks if main content starts within the first 10% of the HTML",
5105
+ description: "Checks how soon main content starts relative to total HTML",
5053
5106
  weight: 0.5,
5054
5107
  run: async (ctx) => {
5055
5108
  const pages = ctx.sampledPages.slice(0, 10);
@@ -5066,15 +5119,16 @@ var contentStartPosition = {
5066
5119
  url: p.url,
5067
5120
  position: findContentStartPosition(p.html)
5068
5121
  }));
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) {
5122
+ const medianPct = Math.round(
5123
+ positions.map((p) => p.position).sort((a, b) => a - b)[Math.floor(positions.length / 2)] * 100
5124
+ );
5125
+ if (medianPct <= 30) {
5072
5126
  return {
5073
5127
  id: "content-start-position",
5074
5128
  name: "Content Start Position",
5075
5129
  category: "page-size",
5076
5130
  status: "pass",
5077
- message: `Content starts within first 10% on all ${pages.length} sampled pages (median ${medianPct}%)`,
5131
+ message: `content starts at ${medianPct}% of html (median over ${pages.length} pages)`,
5078
5132
  metadata: { medianPct }
5079
5133
  };
5080
5134
  }
@@ -5082,21 +5136,20 @@ var contentStartPosition = {
5082
5136
  id: "content-start-position",
5083
5137
  name: "Content Start Position",
5084
5138
  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 }
5139
+ status: medianPct <= 50 ? "warn" : "fail",
5140
+ message: `content starts at ${medianPct}% of html (median over ${pages.length} pages)`,
5141
+ 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.",
5142
+ metadata: { medianPct }
5089
5143
  };
5090
5144
  }
5091
5145
  };
5092
5146
  var pageSizeChecks = [
5093
5147
  renderingStrategy,
5148
+ substantialTextContent,
5094
5149
  pageSizeHtml,
5095
5150
  pageSizeMarkdown,
5096
5151
  contentStartPosition
5097
5152
  ];
5098
-
5099
- // node_modules/@agentimization/core/dist/checks/content-structure.js
5100
5153
  var markdownCodeFenceValidity = {
5101
5154
  id: "markdown-code-fence-validity",
5102
5155
  name: "Markdown Code Fence Validity",
@@ -5111,8 +5164,7 @@ var markdownCodeFenceValidity = {
5111
5164
  for (const page of ctx.sampledPages.slice(0, 10)) {
5112
5165
  const codeBlockRegex = /<code[\s\S]*?<\/code>/gi;
5113
5166
  const matches = page.html.match(codeBlockRegex);
5114
- if (matches)
5115
- totalFences2 += matches.length;
5167
+ if (matches) totalFences2 += matches.length;
5116
5168
  }
5117
5169
  if (totalFences2 === 0) {
5118
5170
  return {
@@ -5178,8 +5230,7 @@ var sectionHeaderQuality = {
5178
5230
  const issues = [];
5179
5231
  for (const page of pages) {
5180
5232
  const headings = extractHeadings(page.html);
5181
- if (headings.length === 0)
5182
- continue;
5233
+ if (headings.length === 0) continue;
5183
5234
  let pageGood = true;
5184
5235
  const hasH1 = headings.some((h) => h.level === 1);
5185
5236
  if (!hasH1) {
@@ -5197,8 +5248,7 @@ var sectionHeaderQuality = {
5197
5248
  break;
5198
5249
  }
5199
5250
  }
5200
- if (pageGood)
5201
- goodPages++;
5251
+ if (pageGood) goodPages++;
5202
5252
  }
5203
5253
  if (goodPages === pages.length) {
5204
5254
  return {
@@ -5267,13 +5317,103 @@ var tabbedContentSerialization = {
5267
5317
  };
5268
5318
  }
5269
5319
  };
5320
+ var imageAltText = {
5321
+ id: "image-alt-text",
5322
+ name: "Image Alt Text Coverage",
5323
+ category: "content-structure",
5324
+ description: "Checks that at least 50% of images have descriptive alt text",
5325
+ weight: 0.5,
5326
+ run: async (ctx) => {
5327
+ const pages = ctx.sampledPages.slice(0, 10);
5328
+ if (pages.length === 0) {
5329
+ return {
5330
+ id: "image-alt-text",
5331
+ name: "Image Alt Text Coverage",
5332
+ category: "content-structure",
5333
+ status: "skip",
5334
+ message: "No pages sampled"
5335
+ };
5336
+ }
5337
+ const allImages = pages.flatMap((p) => extractImages(p.html));
5338
+ const contentImages = allImages.filter((img) => img.alt === void 0 || img.alt.trim().length > 0);
5339
+ const decorativeImages = allImages.length - contentImages.length;
5340
+ const withAlt = contentImages.filter((img) => img.alt !== void 0 && img.alt.trim().length > 0).length;
5341
+ if (allImages.length === 0) {
5342
+ return {
5343
+ id: "image-alt-text",
5344
+ name: "Image Alt Text Coverage",
5345
+ category: "content-structure",
5346
+ status: "info",
5347
+ message: `No images found across ${pages.length} sampled pages`
5348
+ };
5349
+ }
5350
+ if (contentImages.length === 0) {
5351
+ return {
5352
+ id: "image-alt-text",
5353
+ name: "Image Alt Text Coverage",
5354
+ category: "content-structure",
5355
+ status: "info",
5356
+ message: `All ${allImages.length} sampled images are decorative (alt="")`,
5357
+ metadata: { decorativeImages, totalImages: allImages.length }
5358
+ };
5359
+ }
5360
+ const ratio = withAlt / contentImages.length;
5361
+ const pct = Math.round(ratio * 100);
5362
+ const summary = `${withAlt}/${contentImages.length} content images have descriptive alt text (${pct}%)${decorativeImages > 0 ? `; ${decorativeImages} decorative skipped` : ""}`;
5363
+ if (ratio >= 0.5) {
5364
+ return {
5365
+ id: "image-alt-text",
5366
+ name: "Image Alt Text Coverage",
5367
+ category: "content-structure",
5368
+ status: "pass",
5369
+ message: summary,
5370
+ metadata: { withAlt, contentImages: contentImages.length, decorativeImages, pct }
5371
+ };
5372
+ }
5373
+ return {
5374
+ id: "image-alt-text",
5375
+ name: "Image Alt Text Coverage",
5376
+ category: "content-structure",
5377
+ status: ratio >= 0.25 ? "warn" : "fail",
5378
+ message: summary,
5379
+ suggestion: `Add descriptive alt text to at least 50% of content images. AI agents and screen readers rely on alt text to understand visual content. Mark purely decorative images with alt="" so they don't dilute the ratio.`,
5380
+ metadata: { withAlt, contentImages: contentImages.length, decorativeImages, pct }
5381
+ };
5382
+ }
5383
+ };
5270
5384
  var contentStructureChecks = [
5271
5385
  markdownCodeFenceValidity,
5272
5386
  sectionHeaderQuality,
5273
- tabbedContentSerialization
5387
+ tabbedContentSerialization,
5388
+ imageAltText
5274
5389
  ];
5275
-
5276
- // node_modules/@agentimization/core/dist/checks/url-stability.js
5390
+ var httpsEnabled = {
5391
+ id: "https-enabled",
5392
+ name: "HTTPS Enabled",
5393
+ category: "url-stability",
5394
+ description: "Checks if the site is served over HTTPS",
5395
+ weight: 0.7,
5396
+ requiresNetwork: true,
5397
+ run: async (ctx) => {
5398
+ if (ctx.baseUrl.protocol === "https:") {
5399
+ return {
5400
+ id: "https-enabled",
5401
+ name: "HTTPS Enabled",
5402
+ category: "url-stability",
5403
+ status: "pass",
5404
+ message: "Site is served over HTTPS"
5405
+ };
5406
+ }
5407
+ return {
5408
+ id: "https-enabled",
5409
+ name: "HTTPS Enabled",
5410
+ category: "url-stability",
5411
+ status: "fail",
5412
+ message: `Site is served over ${ctx.baseUrl.protocol.replace(":", "")} \u2014 AI crawlers de-prioritize non-HTTPS sources`,
5413
+ suggestion: "Serve your site over HTTPS. AI crawlers like GPTBot, ClaudeBot, and PerplexityBot strongly prefer HTTPS and may skip plain HTTP entirely."
5414
+ };
5415
+ }
5416
+ };
5277
5417
  var httpStatusCodes = {
5278
5418
  id: "http-status-codes",
5279
5419
  name: "HTTP Status Codes",
@@ -5290,8 +5430,7 @@ var httpStatusCodes = {
5290
5430
  for (const url of testUrls) {
5291
5431
  try {
5292
5432
  const resp = await fetch(url, { method: "GET", redirect: "follow" });
5293
- if (resp.status === 404)
5294
- proper404++;
5433
+ if (resp.status === 404) proper404++;
5295
5434
  } catch {
5296
5435
  }
5297
5436
  }
@@ -5386,12 +5525,11 @@ var cacheHeaderHygiene = {
5386
5525
  }
5387
5526
  };
5388
5527
  var urlStabilityChecks = [
5528
+ httpsEnabled,
5389
5529
  httpStatusCodes,
5390
5530
  redirectBehavior,
5391
5531
  cacheHeaderHygiene
5392
5532
  ];
5393
-
5394
- // node_modules/@agentimization/core/dist/checks/authentication.js
5395
5533
  var authGateDetection = {
5396
5534
  id: "auth-gate-detection",
5397
5535
  name: "Auth Gate Detection",
@@ -5439,7 +5577,9 @@ var authAlternativeAccess = {
5439
5577
  requiresNetwork: true,
5440
5578
  run: async (ctx) => {
5441
5579
  const pages = ctx.sampledPages.slice(0, 10);
5442
- const gated = pages.filter((p) => p.statusCode === 401 || p.statusCode === 403);
5580
+ const gated = pages.filter(
5581
+ (p) => p.statusCode === 401 || p.statusCode === 403
5582
+ );
5443
5583
  if (gated.length === 0) {
5444
5584
  return {
5445
5585
  id: "auth-alternative-access",
@@ -5474,28 +5614,18 @@ var authenticationChecks = [
5474
5614
  authGateDetection,
5475
5615
  authAlternativeAccess
5476
5616
  ];
5477
-
5478
- // node_modules/@agentimization/core/dist/checks/geo-signals.js
5479
5617
  var detectFramework = (pages) => {
5480
5618
  for (const page of pages) {
5481
5619
  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";
5620
+ if (xpb.includes("next.js")) return "next";
5621
+ if (xpb.includes("nuxt")) return "nuxt";
5486
5622
  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";
5623
+ if (/\/_next\/static\//.test(html) || /<script[^>]+id="__NEXT_DATA__"/.test(html)) return "next";
5624
+ if (/__NUXT__\s*=/.test(html) || /\/_nuxt\//.test(html)) return "nuxt";
5625
+ if (/data-sveltekit-/.test(html)) return "sveltekit";
5626
+ if (/<meta\s+name="generator"\s+content="Astro/i.test(html)) return "astro";
5627
+ if (/<meta\s+name="generator"\s+content="WordPress/i.test(html)) return "wordpress";
5628
+ if (/\/build\/_assets\/.*\.js/.test(html) && /window\.__remixContext/.test(html)) return "remix";
5499
5629
  }
5500
5630
  return null;
5501
5631
  };
@@ -5508,8 +5638,7 @@ var FRAMEWORK_DOCS = {
5508
5638
  wordpress: "https://yoast.com/structured-data-with-schema-org-the-ultimate-guide/"
5509
5639
  };
5510
5640
  var frameworkHint = (fw) => {
5511
- if (fw && FRAMEWORK_DOCS[fw])
5512
- return ` See: ${FRAMEWORK_DOCS[fw]}`;
5641
+ if (fw && FRAMEWORK_DOCS[fw]) return ` See: ${FRAMEWORK_DOCS[fw]}`;
5513
5642
  return " See: https://schema.org/docs/gs.html";
5514
5643
  };
5515
5644
  var structuredDataCoverage = {
@@ -5528,8 +5657,7 @@ var structuredDataCoverage = {
5528
5657
  withStructuredData++;
5529
5658
  for (const item of jsonLd) {
5530
5659
  const type = item?.["@type"];
5531
- if (typeof type === "string")
5532
- types.push(type);
5660
+ if (typeof type === "string") types.push(type);
5533
5661
  }
5534
5662
  }
5535
5663
  }
@@ -5606,8 +5734,7 @@ var citationWorthiness = {
5606
5734
  signals.withLists++;
5607
5735
  pageCitable = true;
5608
5736
  }
5609
- if (pageCitable)
5610
- citablePages++;
5737
+ if (pageCitable) citablePages++;
5611
5738
  }
5612
5739
  if (citablePages >= pages.length * 0.7) {
5613
5740
  return {
@@ -5640,9 +5767,10 @@ var topicalAuthoritySignals = {
5640
5767
  const pages = ctx.sampledPages.slice(0, 10);
5641
5768
  let totalInternalLinks = 0;
5642
5769
  let pagesWithGoodLinking = 0;
5770
+ const resolveBase = ctx.mode === "local" ? ctx.baseUrl.href : ctx.baseUrl.origin;
5643
5771
  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) => {
5772
+ const links = extractLinks(page.html, resolveBase);
5773
+ const internalLinks = ctx.mode === "local" ? links.filter((l) => l.startsWith("file:")) : links.filter((l) => {
5646
5774
  try {
5647
5775
  return new URL(l).origin === ctx.baseUrl.origin;
5648
5776
  } catch {
@@ -5650,8 +5778,7 @@ var topicalAuthoritySignals = {
5650
5778
  }
5651
5779
  });
5652
5780
  totalInternalLinks += internalLinks.length;
5653
- if (internalLinks.length >= 3)
5654
- pagesWithGoodLinking++;
5781
+ if (internalLinks.length >= 3) pagesWithGoodLinking++;
5655
5782
  }
5656
5783
  const avgLinks = pages.length > 0 ? Math.round(totalInternalLinks / pages.length) : 0;
5657
5784
  if (avgLinks >= 5 && pagesWithGoodLinking >= pages.length * 0.7) {
@@ -5737,12 +5864,11 @@ var eeatSignals = {
5737
5864
  return !!obj?.author;
5738
5865
  });
5739
5866
  const hasAuthorHtml = /class=["'][^"']*author[^"']*["']|rel=["']author["']/i.test(page.html);
5740
- if (hasAuthorMeta || hasAuthorJsonLd || hasAuthorHtml)
5741
- withAuthor++;
5867
+ if (hasAuthorMeta || hasAuthorJsonLd || hasAuthorHtml) withAuthor++;
5742
5868
  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++;
5869
+ const linkBase = ctx.mode === "local" ? ctx.baseUrl.href : ctx.baseUrl.origin;
5870
+ const hasAboutPage = extractLinks(page.html, linkBase).some((l) => /about|team|author/i.test(l));
5871
+ if (hasCredentials || hasAboutPage) withExpertise++;
5746
5872
  }
5747
5873
  const score = (withAuthor + withExpertise) / (pages.length * 2);
5748
5874
  if (score >= 0.6) {
@@ -5782,8 +5908,7 @@ var faqSchema = {
5782
5908
  return type === "FAQPage" || type === "QAPage";
5783
5909
  });
5784
5910
  const hasFaqHtml = /<details|<summary|class=["'][^"']*faq[^"']*["']|id=["'][^"']*faq[^"']*["']/i.test(page.html);
5785
- if (hasFaqSchema || hasFaqHtml)
5786
- withFaq++;
5911
+ if (hasFaqSchema || hasFaqHtml) withFaq++;
5787
5912
  }
5788
5913
  if (withFaq > 0) {
5789
5914
  return {
@@ -5806,6 +5931,185 @@ var faqSchema = {
5806
5931
  };
5807
5932
  }
5808
5933
  };
5934
+ var metaDescription = {
5935
+ id: "meta-description",
5936
+ name: "Meta Description",
5937
+ category: "geo-signals",
5938
+ description: "Checks for a meta description between 50 and 160 characters",
5939
+ weight: 0.5,
5940
+ run: async (ctx) => {
5941
+ const pages = ctx.sampledPages.slice(0, 10);
5942
+ if (pages.length === 0) {
5943
+ return {
5944
+ id: "meta-description",
5945
+ name: "Meta Description",
5946
+ category: "geo-signals",
5947
+ status: "skip",
5948
+ message: "No pages sampled"
5949
+ };
5950
+ }
5951
+ let withGoodDescription = 0;
5952
+ let missing = 0;
5953
+ let tooShort = 0;
5954
+ let tooLong = 0;
5955
+ for (const page of pages) {
5956
+ const meta = extractMetaTags(page.html);
5957
+ const description = meta["description"]?.trim();
5958
+ if (!description) {
5959
+ missing++;
5960
+ continue;
5961
+ }
5962
+ const len = description.length;
5963
+ if (len >= 50 && len <= 160) withGoodDescription++;
5964
+ else if (len < 50) tooShort++;
5965
+ else tooLong++;
5966
+ }
5967
+ if (withGoodDescription === pages.length) {
5968
+ return {
5969
+ id: "meta-description",
5970
+ name: "Meta Description",
5971
+ category: "geo-signals",
5972
+ status: "pass",
5973
+ message: `All ${pages.length} pages have a meta description between 50\u2013160 characters`,
5974
+ metadata: { withGoodDescription }
5975
+ };
5976
+ }
5977
+ if (missing === pages.length) {
5978
+ return {
5979
+ id: "meta-description",
5980
+ name: "Meta Description",
5981
+ category: "geo-signals",
5982
+ status: "fail",
5983
+ message: "No meta description found on any sampled page",
5984
+ suggestion: 'Add a <meta name="description"> between 50 and 160 characters to every page. Generative engines quote meta descriptions when summarizing your content.'
5985
+ };
5986
+ }
5987
+ const detail = [
5988
+ missing > 0 ? `${missing} missing` : null,
5989
+ tooShort > 0 ? `${tooShort} too short` : null,
5990
+ tooLong > 0 ? `${tooLong} too long` : null
5991
+ ].filter(Boolean).join(" \xB7 ");
5992
+ return {
5993
+ id: "meta-description",
5994
+ name: "Meta Description",
5995
+ category: "geo-signals",
5996
+ status: missing >= pages.length / 2 ? "fail" : "warn",
5997
+ message: `${withGoodDescription}/${pages.length} pages have meta descriptions in the 50\u2013160 char range${detail ? ` \xB7 ${detail}` : ""}`,
5998
+ suggestion: missing > 0 ? 'Add a <meta name="description"> between 50 and 160 characters to every page. Some pages are missing it entirely.' : "Aim for 50\u2013160 characters. Shorter descriptions lack context for AI; longer ones get truncated.",
5999
+ metadata: { withGoodDescription, missing, tooShort, tooLong }
6000
+ };
6001
+ }
6002
+ };
6003
+ var openGraphTags = {
6004
+ id: "open-graph-tags",
6005
+ name: "Open Graph Tags",
6006
+ category: "geo-signals",
6007
+ description: "Checks for og:title, og:description, og:image, and og:url",
6008
+ weight: 0.5,
6009
+ run: async (ctx) => {
6010
+ const pages = ctx.sampledPages.slice(0, 10);
6011
+ if (pages.length === 0) {
6012
+ return {
6013
+ id: "open-graph-tags",
6014
+ name: "Open Graph Tags",
6015
+ category: "geo-signals",
6016
+ status: "skip",
6017
+ message: "No pages sampled"
6018
+ };
6019
+ }
6020
+ const required = ["og:title", "og:description", "og:image", "og:url"];
6021
+ let fullCoverage = 0;
6022
+ let partialCoverage = 0;
6023
+ const missingCounts = { "og:title": 0, "og:description": 0, "og:image": 0, "og:url": 0 };
6024
+ for (const page of pages) {
6025
+ const meta = extractMetaTags(page.html);
6026
+ const missing = required.filter((tag) => !meta[tag]);
6027
+ for (const tag of missing) missingCounts[tag] = (missingCounts[tag] ?? 0) + 1;
6028
+ if (missing.length === 0) fullCoverage++;
6029
+ else if (missing.length < required.length) partialCoverage++;
6030
+ }
6031
+ if (fullCoverage === pages.length) {
6032
+ return {
6033
+ id: "open-graph-tags",
6034
+ name: "Open Graph Tags",
6035
+ category: "geo-signals",
6036
+ status: "pass",
6037
+ message: `All ${pages.length} pages have complete Open Graph tags`
6038
+ };
6039
+ }
6040
+ const mostMissing = Object.entries(missingCounts).filter(([, n]) => n > 0).sort(([, a], [, b]) => b - a).map(([tag]) => tag);
6041
+ const noneCovered = pages.length - fullCoverage - partialCoverage;
6042
+ return {
6043
+ id: "open-graph-tags",
6044
+ name: "Open Graph Tags",
6045
+ category: "geo-signals",
6046
+ status: fullCoverage + partialCoverage === 0 ? "fail" : "warn",
6047
+ message: `${fullCoverage}/${pages.length} pages have complete Open Graph tags${partialCoverage > 0 ? ` \xB7 ${partialCoverage} partial` : ""}${noneCovered > 0 ? ` \xB7 ${noneCovered} with none` : ""}${mostMissing.length > 0 ? ` \xB7 most often missing: ${mostMissing.slice(0, 2).join(", ")}` : ""}`,
6048
+ suggestion: "Add og:title, og:description, og:image, and og:url to every page. AI engines and link previews use these to render rich citations of your content.",
6049
+ metadata: { fullCoverage, partialCoverage, noneCovered, missingCounts }
6050
+ };
6051
+ }
6052
+ };
6053
+ var externalCitations = {
6054
+ id: "external-citations",
6055
+ name: "External Citations",
6056
+ category: "geo-signals",
6057
+ description: "Checks for at least 2 outbound links to external sources per page",
6058
+ weight: 0.5,
6059
+ run: async (ctx) => {
6060
+ if (ctx.mode === "local") {
6061
+ return {
6062
+ id: "external-citations",
6063
+ name: "External Citations",
6064
+ category: "geo-signals",
6065
+ status: "info",
6066
+ message: "External link detection requires a live origin to compare against"
6067
+ };
6068
+ }
6069
+ const pages = ctx.sampledPages.slice(0, 10);
6070
+ if (pages.length === 0) {
6071
+ return {
6072
+ id: "external-citations",
6073
+ name: "External Citations",
6074
+ category: "geo-signals",
6075
+ status: "skip",
6076
+ message: "No pages sampled"
6077
+ };
6078
+ }
6079
+ const origin = ctx.baseUrl.origin;
6080
+ let pagesWithCitations = 0;
6081
+ let totalExternal = 0;
6082
+ for (const page of pages) {
6083
+ const links = extractLinks(page.html, origin);
6084
+ const external = links.filter((l) => {
6085
+ const u = new URL(l);
6086
+ return u.protocol.startsWith("http") && u.origin !== origin;
6087
+ });
6088
+ totalExternal += external.length;
6089
+ if (external.length >= 2) pagesWithCitations++;
6090
+ }
6091
+ const avgExternal = Math.round(totalExternal / pages.length);
6092
+ if (pagesWithCitations >= pages.length * 0.7) {
6093
+ return {
6094
+ id: "external-citations",
6095
+ name: "External Citations",
6096
+ category: "geo-signals",
6097
+ status: "pass",
6098
+ message: `${pagesWithCitations}/${pages.length} pages have \u22652 outbound links (avg ${avgExternal}/page)`,
6099
+ metadata: { pagesWithCitations, avgExternal }
6100
+ };
6101
+ }
6102
+ return {
6103
+ id: "external-citations",
6104
+ name: "External Citations",
6105
+ category: "geo-signals",
6106
+ status: pagesWithCitations > 0 ? "warn" : "fail",
6107
+ message: `Only ${pagesWithCitations}/${pages.length} pages have \u22652 outbound links (avg ${avgExternal}/page)`,
6108
+ suggestion: "Add at least 2 outbound links to authoritative external sources per page. Citing sources signals credibility to generative engines, which weigh outbound links when deciding what to cite.",
6109
+ metadata: { pagesWithCitations, avgExternal }
6110
+ };
6111
+ }
6112
+ };
5809
6113
  var canonicalUrlConsistency = {
5810
6114
  id: "canonical-url-consistency",
5811
6115
  name: "Canonical URL Consistency",
@@ -5813,6 +6117,15 @@ var canonicalUrlConsistency = {
5813
6117
  description: "Checks if pages have consistent canonical URLs",
5814
6118
  weight: 0.5,
5815
6119
  run: async (ctx) => {
6120
+ if (ctx.mode === "local") {
6121
+ return {
6122
+ id: "canonical-url-consistency",
6123
+ name: "Canonical URL Consistency",
6124
+ category: "geo-signals",
6125
+ status: "info",
6126
+ message: "only meaningful for live urls. re-run against a deployed site to verify"
6127
+ };
6128
+ }
5816
6129
  const pages = ctx.sampledPages.slice(0, 10);
5817
6130
  let withCanonical = 0;
5818
6131
  let selfReferencing = 0;
@@ -5867,10 +6180,11 @@ var geoSignalChecks = [
5867
6180
  contentFreshness,
5868
6181
  eeatSignals,
5869
6182
  faqSchema,
6183
+ metaDescription,
6184
+ openGraphTags,
6185
+ externalCitations,
5870
6186
  canonicalUrlConsistency
5871
6187
  ];
5872
-
5873
- // node_modules/@agentimization/core/dist/checks/agent-protocols.js
5874
6188
  var mcpServerCard = {
5875
6189
  id: "mcp-server-card",
5876
6190
  name: "MCP Server Card",
@@ -6004,12 +6318,9 @@ var contentSignals = {
6004
6318
  }
6005
6319
  const text = ctx.robotsTxt.toLowerCase();
6006
6320
  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");
6321
+ if (/ai[-_]?train/i.test(ctx.robotsTxt)) signals.push("ai-train");
6322
+ if (/ai[-_]?input/i.test(ctx.robotsTxt)) signals.push("ai-input");
6323
+ if (/content[-_]?signals?/i.test(ctx.robotsTxt)) signals.push("content-signals");
6013
6324
  const hasGPTBot = text.includes("gptbot");
6014
6325
  const hasClaudeBot = text.includes("claudebot") || text.includes("claude-web");
6015
6326
  const hasGoogleExtended = text.includes("google-extended");
@@ -6244,8 +6555,6 @@ var agentProtocolChecks = [
6244
6555
  agentSkillsIndex,
6245
6556
  agentsMd
6246
6557
  ];
6247
-
6248
- // node_modules/@agentimization/core/dist/checks/index.js
6249
6558
  var ALL_CHECKS = [
6250
6559
  ...contentDiscoverabilityChecks,
6251
6560
  ...markdownAvailabilityChecks,
@@ -6256,13 +6565,31 @@ var ALL_CHECKS = [
6256
6565
  ...geoSignalChecks,
6257
6566
  ...agentProtocolChecks
6258
6567
  ];
6259
-
6260
- // node_modules/@agentimization/core/dist/utils/local.js
6261
- import { readFileSync, readdirSync, existsSync } from "fs";
6262
- import { join, relative, extname } from "path";
6568
+ var readIfExists = (path) => {
6569
+ try {
6570
+ if (existsSync(path)) {
6571
+ return readFileSync(path, "utf-8");
6572
+ }
6573
+ } catch {
6574
+ }
6575
+ return void 0;
6576
+ };
6577
+ var findUpward = (start, names, maxDepth = 6) => {
6578
+ let current = start;
6579
+ for (let i = 0; i < maxDepth; i++) {
6580
+ for (const name of names) {
6581
+ const candidate = join(current, name);
6582
+ const value = readIfExists(candidate);
6583
+ if (value !== void 0) return value;
6584
+ }
6585
+ const parent = dirname(current);
6586
+ if (parent === current) break;
6587
+ current = parent;
6588
+ }
6589
+ return void 0;
6590
+ };
6263
6591
  var walkDir = (dir, extensions, maxDepth = 10) => {
6264
- if (maxDepth <= 0)
6265
- return [];
6592
+ if (maxDepth <= 0) return [];
6266
6593
  const results = [];
6267
6594
  try {
6268
6595
  const entries = readdirSync(dir, { withFileTypes: true });
@@ -6281,15 +6608,6 @@ var walkDir = (dir, extensions, maxDepth = 10) => {
6281
6608
  }
6282
6609
  return results;
6283
6610
  };
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
6611
  var buildLocalContext = (dirPath, config) => {
6294
6612
  const baseUrl = new URL(`file://${dirPath}`);
6295
6613
  const robotsTxt = readIfExists(join(dirPath, "robots.txt"));
@@ -6299,7 +6617,7 @@ var buildLocalContext = (dirPath, config) => {
6299
6617
  const mcpServerCard2 = readIfExists(join(dirPath, ".well-known", "mcp", "server-card.json"));
6300
6618
  const apiCatalog2 = readIfExists(join(dirPath, ".well-known", "api-catalog"));
6301
6619
  const agentSkillsIndex2 = readIfExists(join(dirPath, ".well-known", "agent-skills", "index.json"));
6302
- const agentsMd2 = readIfExists(join(dirPath, "AGENTS.md")) ?? readIfExists(join(dirPath, "AGENT.md"));
6620
+ const agentsMd2 = findUpward(dirPath, ["AGENTS.md", "AGENT.md"]);
6303
6621
  const sitemapUrls = sitemapXml ? parseSitemapUrls(sitemapXml) : [];
6304
6622
  if (!sitemapXml && robotsTxt) {
6305
6623
  const sitemapMatch = robotsTxt.match(/Sitemap:\s*(.+)/i);
@@ -6316,8 +6634,8 @@ var buildLocalContext = (dirPath, config) => {
6316
6634
  }
6317
6635
  const htmlFiles = walkDir(dirPath, /* @__PURE__ */ new Set([".html", ".htm"]));
6318
6636
  const mdFiles = walkDir(dirPath, /* @__PURE__ */ new Set([".md", ".mdx"]));
6319
- const allFiles = [...htmlFiles, ...mdFiles];
6320
- const sampled = allFiles.slice(0, config.sampleSize);
6637
+ const sampleSource = htmlFiles.length > 0 ? htmlFiles : mdFiles;
6638
+ const sampled = sampleSource.slice(0, config.sampleSize);
6321
6639
  const sampledPages = sampled.map((filePath) => {
6322
6640
  const content = readFileSync(filePath, "utf-8");
6323
6641
  const relPath = relative(dirPath, filePath);
@@ -6368,19 +6686,12 @@ var wrapMarkdownAsHtml = (md, title) => {
6368
6686
  html = html.replace(/^([^<\n].+)$/gm, "<p>$1</p>");
6369
6687
  return `<!DOCTYPE html><html><head><title>${title}</title></head><body><main>${html}</main></body></html>`;
6370
6688
  };
6371
-
6372
- // node_modules/@agentimization/core/dist/index.js
6373
6689
  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";
6690
+ if (score >= 95) return "A+";
6691
+ if (score >= 85) return "A";
6692
+ if (score >= 70) return "B";
6693
+ if (score >= 55) return "C";
6694
+ if (score >= 40) return "D";
6384
6695
  return "F";
6385
6696
  };
6386
6697
  var buildRemoteContext = async (targetUrl, config) => {
@@ -6389,7 +6700,15 @@ var buildRemoteContext = async (targetUrl, config) => {
6389
6700
  emit({ type: "phase", phase: "fetching" });
6390
6701
  const baseUrl = new URL(targetUrl);
6391
6702
  const origin = baseUrl.origin;
6392
- const [robotsResult, llmsResult, llmsFullResult, sitemapResult, mcpCardResult, apiCatalogResult, agentSkillsResult] = await Promise.allSettled([
6703
+ const [
6704
+ robotsResult,
6705
+ llmsResult,
6706
+ llmsFullResult,
6707
+ sitemapResult,
6708
+ mcpCardResult,
6709
+ apiCatalogResult,
6710
+ agentSkillsResult
6711
+ ] = await Promise.allSettled([
6393
6712
  fetchText(`${origin}/robots.txt`, config),
6394
6713
  fetchText(`${origin}/llms.txt`, config),
6395
6714
  fetchText(`${origin}/llms-full.txt`, config),
@@ -6523,7 +6842,9 @@ var runAudit = async (ctx, config, start) => {
6523
6842
  overallScore = 50;
6524
6843
  }
6525
6844
  const categories = {};
6526
- const activeCategories = config.categories.filter((cat) => results.some((r) => r.category === cat));
6845
+ const activeCategories = config.categories.filter(
6846
+ (cat) => results.some((r) => r.category === cat)
6847
+ );
6527
6848
  for (const cat of activeCategories) {
6528
6849
  const catResults = results.filter((r) => r.category === cat);
6529
6850
  const catScorable = catResults.filter((r) => r.status !== "skip" && r.status !== "info");
@@ -7230,11 +7551,11 @@ var copyToClipboard = async (text) => {
7230
7551
  // src/ui/action-menu.tsx
7231
7552
  import { writeFileSync } from "fs";
7232
7553
  import { resolve } from "path";
7233
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7554
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7234
7555
  var FEEDBACK_OK = "oklch(0.858 0.109 142.7)";
7235
7556
  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." },
7557
+ var buildMenuOptions = (hasIssues) => [
7558
+ ...hasIssues ? [{ label: "Copy fix prompt to clipboard", value: "copy", hint: "paste into Claude, ChatGPT, etc." }] : [],
7238
7559
  { label: "Save JSON report", value: "json", hint: "agentimization-report.json" },
7239
7560
  { label: "Run another URL or path", value: "retry" },
7240
7561
  { label: "Exit", value: "exit" }
@@ -7251,6 +7572,8 @@ var ActionMenu = ({
7251
7572
  const [feedback, setFeedback] = useState2(null);
7252
7573
  const [input, setInput] = useState2("");
7253
7574
  const [done, setDone] = useState2(false);
7575
+ const hasIssues = result.summary.failed > 0 || result.summary.warned > 0;
7576
+ const menuOptions = buildMenuOptions(hasIssues);
7254
7577
  const handleAction = (action) => {
7255
7578
  const promptOpts = { mode: isLocal ? "local" : "remote", target };
7256
7579
  switch (action) {
@@ -7285,6 +7608,7 @@ var ActionMenu = ({
7285
7608
  setDone(true);
7286
7609
  setTimeout(() => {
7287
7610
  exit();
7611
+ process.stdout.write("\n");
7288
7612
  if (result.overall_score < 50) process.exit(1);
7289
7613
  }, 50);
7290
7614
  break;
@@ -7297,13 +7621,16 @@ var ActionMenu = ({
7297
7621
  if (key.upArrow) {
7298
7622
  setSelected((prev) => Math.max(0, prev - 1));
7299
7623
  } else if (key.downArrow) {
7300
- setSelected((prev) => Math.min(MENU_OPTIONS.length - 1, prev + 1));
7624
+ setSelected((prev) => Math.min(menuOptions.length - 1, prev + 1));
7301
7625
  } else if (key.return) {
7302
- const option = MENU_OPTIONS[selected];
7626
+ const option = menuOptions[selected];
7303
7627
  handleAction(option.value);
7304
7628
  } else if (char === "q" || key.escape) {
7305
7629
  setDone(true);
7306
- setTimeout(() => exit(), 50);
7630
+ setTimeout(() => {
7631
+ exit();
7632
+ process.stdout.write("\n");
7633
+ }, 50);
7307
7634
  }
7308
7635
  return;
7309
7636
  }
@@ -7337,7 +7664,7 @@ var ActionMenu = ({
7337
7664
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
7338
7665
  /* @__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
7666
  /* @__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) => {
7667
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: menuOptions.map((opt, i) => {
7341
7668
  const isSelected = i === selected;
7342
7669
  return /* @__PURE__ */ jsxs4(Box4, { paddingLeft: 2, children: [
7343
7670
  /* @__PURE__ */ jsxs4(Text4, { color: isSelected ? toInkColor(ACCENT_BLUE) : void 0, children: [
@@ -7352,7 +7679,10 @@ var ActionMenu = ({
7352
7679
  ] }) : null
7353
7680
  ] }, opt.value);
7354
7681
  }) }),
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
7682
+ feedback ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
7683
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { color: toInkColor(feedback.startsWith("\u2713") ? FEEDBACK_OK : FEEDBACK_ERR), children: feedback }) }),
7684
+ /* @__PURE__ */ jsx4(Box4, { height: 1, children: /* @__PURE__ */ jsx4(Text4, { children: " " }) })
7685
+ ] }) : null
7356
7686
  ] });
7357
7687
  };
7358
7688
 
@@ -7433,7 +7763,7 @@ var ErrorActions = ({ onRetry }) => {
7433
7763
 
7434
7764
  // src/ui/target.ts
7435
7765
  import { existsSync as existsSync2, statSync } from "fs";
7436
- import { resolve as resolve2 } from "path";
7766
+ import { join as join2, resolve as resolve2 } from "path";
7437
7767
  var isLocalPath = (arg) => {
7438
7768
  if (arg === "." || arg === ".." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/")) {
7439
7769
  return true;
@@ -7449,11 +7779,30 @@ var normalizeUrl = (arg) => {
7449
7779
  const parsed = new URL(arg.startsWith("http") ? arg : `https://${arg}`);
7450
7780
  return parsed.href;
7451
7781
  };
7782
+ var BUILD_DIR_NAMES = ["dist", "build", "out", "_site", "public"];
7783
+ var hasIndex = (dir) => ["index.html", "index.htm", "index.md"].some((name) => existsSync2(join2(dir, name)));
7784
+ var findBuildDir = (root) => {
7785
+ for (const name of BUILD_DIR_NAMES) {
7786
+ const candidate = join2(root, name);
7787
+ if (existsSync2(candidate) && statSync(candidate).isDirectory() && hasIndex(candidate)) {
7788
+ return candidate;
7789
+ }
7790
+ }
7791
+ return void 0;
7792
+ };
7452
7793
  var resolveTarget = (input) => {
7453
7794
  const trimmed = input.trim();
7454
7795
  if (!trimmed) return { error: "Please enter a URL or path." };
7455
7796
  if (isLocalPath(trimmed)) {
7456
- return { target: resolve2(trimmed), isLocal: true };
7797
+ const requested = resolve2(trimmed);
7798
+ if (hasIndex(requested)) {
7799
+ return { target: requested, isLocal: true };
7800
+ }
7801
+ const buildDir = findBuildDir(requested);
7802
+ if (buildDir) {
7803
+ return { target: buildDir, isLocal: true, autoDetectedFrom: requested };
7804
+ }
7805
+ return { target: requested, isLocal: true };
7457
7806
  }
7458
7807
  try {
7459
7808
  return { target: normalizeUrl(trimmed), isLocal: false };
@@ -7464,7 +7813,13 @@ var resolveTarget = (input) => {
7464
7813
 
7465
7814
  // src/ui/app.tsx
7466
7815
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
7467
- var App = ({ target: initialTarget, isLocal: initialIsLocal, categories, sampleSize }) => {
7816
+ var App = ({
7817
+ target: initialTarget,
7818
+ isLocal: initialIsLocal,
7819
+ categories,
7820
+ sampleSize,
7821
+ autoDetectedFrom
7822
+ }) => {
7468
7823
  const [target, setTarget] = useState4(initialTarget);
7469
7824
  const [isLocal, setIsLocal] = useState4(initialIsLocal);
7470
7825
  const [phase, setPhase] = useState4("init");
@@ -7498,7 +7853,7 @@ var App = ({ target: initialTarget, isLocal: initialIsLocal, categories, sampleS
7498
7853
  const config = { categories, sampleSize, onEvent };
7499
7854
  const res = isLocal ? await auditLocal(target, config) : await audit(target, config);
7500
7855
  if (isLocal) {
7501
- setNetworkSkipped(35 - res.summary.total);
7856
+ setNetworkSkipped(ALL_CHECKS.length - res.summary.total);
7502
7857
  }
7503
7858
  setResult(res);
7504
7859
  setPhase("done");
@@ -7573,6 +7928,11 @@ var App = ({ target: initialTarget, isLocal: initialIsLocal, categories, sampleS
7573
7928
  gradeColor: result ? GRADE_COLORS[result.grade] : void 0
7574
7929
  }
7575
7930
  ) }),
7931
+ autoDetectedFrom ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
7932
+ "auto-detected build output (was ",
7933
+ autoDetectedFrom,
7934
+ ")"
7935
+ ] }) : null,
7576
7936
  isLocal && networkSkipped > 0 && phase === "done" ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
7577
7937
  networkSkipped,
7578
7938
  " network-only checks skipped"
@@ -7636,10 +7996,18 @@ Examples:
7636
7996
  $ agentimization ./docs --category content-structure
7637
7997
  $ agentimization ./public --md | pbcopy`).action(async (target, opts) => {
7638
7998
  const categories = opts.category ? [opts.category] : void 0;
7639
- const isLocal = isLocalPath(target);
7999
+ const resolved = resolveTarget(target);
8000
+ if ("error" in resolved) {
8001
+ console.error(`Error: ${resolved.error}`);
8002
+ return process.exit(1);
8003
+ }
8004
+ const { target: targetResolved, isLocal, autoDetectedFrom } = resolved;
8005
+ if (autoDetectedFrom && !opts.json) {
8006
+ console.error(`auditing built output at ${targetResolved}`);
8007
+ }
7640
8008
  if (opts.json) {
7641
8009
  try {
7642
- const result = isLocal ? await auditLocal(resolve3(target), { categories, sampleSize: opts.sampleSize }) : await audit(normalizeUrl2(target), { categories, sampleSize: opts.sampleSize });
8010
+ const result = isLocal ? await auditLocal(targetResolved, { categories, sampleSize: opts.sampleSize }) : await audit(targetResolved, { categories, sampleSize: opts.sampleSize });
7643
8011
  console.log(JSON.stringify(result, null, 2));
7644
8012
  if (result.overall_score < 50) process.exit(1);
7645
8013
  } catch (error) {
@@ -7651,8 +8019,8 @@ Examples:
7651
8019
  }
7652
8020
  if (opts.md) {
7653
8021
  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 }));
8022
+ const result = isLocal ? await auditLocal(targetResolved, { categories, sampleSize: opts.sampleSize }) : await audit(targetResolved, { categories, sampleSize: opts.sampleSize });
8023
+ console.log(generateAgentPrompt(result, { mode: isLocal ? "local" : "remote", target: targetResolved }));
7656
8024
  if (result.overall_score < 50) process.exit(1);
7657
8025
  } catch (error) {
7658
8026
  const msg = error instanceof Error ? error.message : "Unknown error";
@@ -7661,24 +8029,16 @@ Examples:
7661
8029
  }
7662
8030
  return;
7663
8031
  }
7664
- const targetResolved = isLocal ? resolve3(target) : normalizeUrl2(target);
7665
8032
  render(
7666
8033
  React2.createElement(App, {
7667
8034
  target: targetResolved,
7668
8035
  isLocal,
7669
8036
  categories,
7670
- sampleSize: opts.sampleSize
8037
+ sampleSize: opts.sampleSize,
8038
+ autoDetectedFrom
7671
8039
  })
7672
8040
  );
7673
8041
  });
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
8042
  try {
7683
8043
  program.parse();
7684
8044
  } catch (err) {