geo-checker 0.3.1 → 0.3.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.
package/dist/cli.js CHANGED
@@ -263,10 +263,12 @@ async function buildContext(url, opts = {}) {
263
263
  "Site appears to be JS-rendered (sparse body + SPA root element). Re-run with --render for accurate results."
264
264
  );
265
265
  }
266
- const [robotsRaw, llmsRaw, llmsFullRaw] = await Promise.all([
266
+ const [robotsRaw, llmsRaw, llmsFullRaw, skillMdRaw, agentPermissionsRaw] = await Promise.all([
267
267
  fetchText(`${origin}/robots.txt`, opts),
268
268
  fetchText(`${origin}/llms.txt`, opts),
269
- fetchText(`${origin}/llms-full.txt`, opts)
269
+ fetchText(`${origin}/llms-full.txt`, opts),
270
+ fetchText(`${origin}/skill.md`, opts),
271
+ fetchText(`${origin}/agent-permissions.json`, opts)
270
272
  ]);
271
273
  let sitemapUrl = null;
272
274
  const robots = robotsRaw ? parseRobots(robotsRaw) : null;
@@ -274,6 +276,13 @@ async function buildContext(url, opts = {}) {
274
276
  if (!sitemapUrl) sitemapUrl = `${origin}/sitemap.xml`;
275
277
  const sitemapRaw = await fetchText(sitemapUrl, opts);
276
278
  const sitemap = sitemapRaw ? parseSitemap(sitemapRaw) : null;
279
+ let agentPermissions = null;
280
+ if (agentPermissionsRaw && agentPermissionsRaw.trim().length > 0) {
281
+ try {
282
+ agentPermissions = JSON.parse(agentPermissionsRaw);
283
+ } catch {
284
+ }
285
+ }
277
286
  return {
278
287
  url,
279
288
  finalUrl,
@@ -288,7 +297,9 @@ async function buildContext(url, opts = {}) {
288
297
  jsonLd: extractJsonLd($),
289
298
  renderMode,
290
299
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
291
- warnings
300
+ warnings,
301
+ skillMd: skillMdRaw && skillMdRaw.trim().length > 0 ? skillMdRaw : null,
302
+ agentPermissions
292
303
  };
293
304
  }
294
305
 
@@ -297,10 +308,11 @@ function defineRule(rule) {
297
308
  return rule;
298
309
  }
299
310
  var CATEGORY_WEIGHTS = {
300
- crawler: 25,
301
- "structured-data": 30,
302
- citation: 25,
303
- content: 20
311
+ crawler: 20,
312
+ "structured-data": 25,
313
+ citation: 20,
314
+ content: 15,
315
+ aeo: 20
304
316
  };
305
317
 
306
318
  // src/engine.ts
@@ -314,7 +326,8 @@ async function runRules(ctx, rules, opts = {}) {
314
326
  crawler: { score: 0, weight: weights.crawler, results: [] },
315
327
  "structured-data": { score: 0, weight: weights["structured-data"], results: [] },
316
328
  citation: { score: 0, weight: weights.citation, results: [] },
317
- content: { score: 0, weight: weights.content, results: [] }
329
+ content: { score: 0, weight: weights.content, results: [] },
330
+ aeo: { score: 0, weight: weights.aeo, results: [] }
318
331
  };
319
332
  for (const rule of rules) {
320
333
  if (onlySet && !onlySet.has(rule.id) && (!rule.stableId || !onlySet.has(rule.stableId))) continue;
@@ -1904,12 +1917,130 @@ var contentRules = [
1904
1917
  externalCitationsRule
1905
1918
  ];
1906
1919
 
1920
+ // src/rules/aeo/skill-md.ts
1921
+ var aeoSkillMdRule = defineRule({
1922
+ id: "aeo.skill-md",
1923
+ stableId: "aeo.skill-md",
1924
+ category: "aeo",
1925
+ group: "opportunity",
1926
+ weight: 3,
1927
+ impact: "high",
1928
+ effort: "low",
1929
+ docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoskill-md",
1930
+ title: "skill.md is present",
1931
+ title_ko: "skill.md \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
1932
+ description: "A /skill.md file describes site capabilities so AI agents know what this site can do for them.",
1933
+ run(ctx) {
1934
+ if (ctx.skillMd !== null) {
1935
+ return {
1936
+ status: "pass",
1937
+ score: 1,
1938
+ rationale: "skill.md found at site root.",
1939
+ rationale_ko: "skill.md\uAC00 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4."
1940
+ };
1941
+ }
1942
+ return {
1943
+ status: "warn",
1944
+ score: 0,
1945
+ rationale: "No /skill.md found. Add one to describe your site capabilities to AI agents.",
1946
+ rationale_ko: "/skill.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5D0\uC774\uC804\uD2B8\uAC00 \uC0AC\uC774\uD2B8 \uAE30\uB2A5\uC744 \uD30C\uC545\uD560 \uC218 \uC788\uB3C4\uB85D \uCD94\uAC00\uD558\uC138\uC694.",
1947
+ fixHint: "Create /skill.md listing what services, products, and capabilities this site offers."
1948
+ };
1949
+ }
1950
+ });
1951
+
1952
+ // src/rules/aeo/agent-permissions.ts
1953
+ var aeoAgentPermissionsRule = defineRule({
1954
+ id: "aeo.agent-permissions",
1955
+ stableId: "aeo.agent-permissions",
1956
+ category: "aeo",
1957
+ group: "opportunity",
1958
+ weight: 3,
1959
+ impact: "medium",
1960
+ effort: "low",
1961
+ docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoagent-permissions",
1962
+ title: "agent-permissions.json is present",
1963
+ title_ko: "agent-permissions.json \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
1964
+ description: "Declares explicit read/summarize/cite/train permissions for AI agents.",
1965
+ run(ctx) {
1966
+ if (ctx.agentPermissions !== null) {
1967
+ return {
1968
+ status: "pass",
1969
+ score: 1,
1970
+ rationale: "agent-permissions.json found at site root.",
1971
+ rationale_ko: "agent-permissions.json\uC774 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4.",
1972
+ evidence: ctx.agentPermissions
1973
+ };
1974
+ }
1975
+ return {
1976
+ status: "warn",
1977
+ score: 0,
1978
+ rationale: "No /agent-permissions.json found. Add one to declare AI agent access policy.",
1979
+ rationale_ko: "/agent-permissions.json\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5D0\uC774\uC804\uD2B8 \uC811\uADFC \uC815\uCC45\uC744 \uBA85\uC2DC\uD558\uB824\uBA74 \uCD94\uAC00\uD558\uC138\uC694.",
1980
+ fixHint: "Create /agent-permissions.json with read, summarize, cite, and train permission flags."
1981
+ };
1982
+ }
1983
+ });
1984
+
1985
+ // src/rules/aeo/token-length.ts
1986
+ var THRESHOLD_OPTIMAL = 15e3;
1987
+ var THRESHOLD_MAX = 25e3;
1988
+ var aeoTokenLengthRule = defineRule({
1989
+ id: "aeo.token-length",
1990
+ stableId: "aeo.token-length",
1991
+ category: "aeo",
1992
+ group: "diagnostic",
1993
+ weight: 4,
1994
+ impact: "medium",
1995
+ effort: "medium",
1996
+ docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeotoken-length",
1997
+ title: "Content token length within AI agent limits",
1998
+ title_ko: "\uCF58\uD150\uCE20 \uD1A0\uD070 \uC218 AI \uC5D0\uC774\uC804\uD2B8 \uAD8C\uC7A5 \uBC94\uC704",
1999
+ description: "Pages under 15K tokens are optimal for AI agents (per Addy Osmani's AEO guidance).",
2000
+ run(ctx) {
2001
+ const text = ctx.$("body").text();
2002
+ const tokenEstimate = Math.round(text.length / 3);
2003
+ const evidence = { tokenEstimate, thresholds: { optimal: THRESHOLD_OPTIMAL, max: THRESHOLD_MAX } };
2004
+ if (tokenEstimate <= THRESHOLD_OPTIMAL) {
2005
+ return {
2006
+ status: "pass",
2007
+ score: 1,
2008
+ rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 within optimal range.`,
2009
+ rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 \uAD8C\uC7A5 \uBC94\uC704(15K) \uC774\uB0B4\uC785\uB2C8\uB2E4.`,
2010
+ evidence
2011
+ };
2012
+ }
2013
+ if (tokenEstimate <= THRESHOLD_MAX) {
2014
+ return {
2015
+ status: "warn",
2016
+ score: 0.5,
2017
+ rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 15K recommendation.`,
2018
+ rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 15K \uAD8C\uC7A5\uCE58\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
2019
+ fixHint: "Consider splitting content into shorter, focused pages.",
2020
+ evidence
2021
+ };
2022
+ }
2023
+ return {
2024
+ status: "fail",
2025
+ score: 0,
2026
+ rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 25K agent processing limit.`,
2027
+ rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 AI \uC5D0\uC774\uC804\uD2B8 \uCC98\uB9AC \uD55C\uACC4(25K)\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
2028
+ fixHint: "Split this page into multiple focused pages under 15K tokens.",
2029
+ evidence
2030
+ };
2031
+ }
2032
+ });
2033
+
2034
+ // src/rules/aeo/index.ts
2035
+ var aeoRules = [aeoSkillMdRule, aeoAgentPermissionsRule, aeoTokenLengthRule];
2036
+
1907
2037
  // src/rules/index.ts
1908
2038
  var defaultRules = [
1909
2039
  ...crawlerRules,
1910
2040
  ...structuredDataRules,
1911
2041
  ...citationRules,
1912
- ...contentRules
2042
+ ...contentRules,
2043
+ ...aeoRules
1913
2044
  ];
1914
2045
 
1915
2046
  // src/config.ts
@@ -2105,7 +2236,8 @@ var CATEGORY_LABELS = {
2105
2236
  crawler: "AI Crawler Access",
2106
2237
  "structured-data": "Structured Data",
2107
2238
  citation: "Citation Signals",
2108
- content: "Content Structure"
2239
+ content: "Content Structure",
2240
+ aeo: "AEO Stack"
2109
2241
  };
2110
2242
  function scoreBadge(score) {
2111
2243
  const color = score >= 85 ? "brightgreen" : score >= 60 ? "yellow" : "red";
@@ -2400,7 +2532,8 @@ var CATEGORY_LABELS2 = {
2400
2532
  crawler: "AI Crawler Access",
2401
2533
  "structured-data": "Structured Data",
2402
2534
  citation: "Citation Signals",
2403
- content: "Content Structure"
2535
+ content: "Content Structure",
2536
+ aeo: "AEO Stack"
2404
2537
  };
2405
2538
  function colorScore(score) {
2406
2539
  if (score >= 85) return kleur.green().bold(`${score}`);
@@ -2500,7 +2633,8 @@ var CATEGORY_LABELS3 = {
2500
2633
  crawler: "AI Crawler Access",
2501
2634
  "structured-data": "Structured Data",
2502
2635
  citation: "Citation Signals",
2503
- content: "Content Structure"
2636
+ content: "Content Structure",
2637
+ aeo: "AEO Stack"
2504
2638
  };
2505
2639
  var IMPACT_ORDER = {
2506
2640
  critical: 4,
@@ -2583,13 +2717,15 @@ function partitionResults(report) {
2583
2717
  crawler: [],
2584
2718
  "structured-data": [],
2585
2719
  citation: [],
2586
- content: []
2720
+ content: [],
2721
+ aeo: []
2587
2722
  };
2588
2723
  const passed = {
2589
2724
  crawler: [],
2590
2725
  "structured-data": [],
2591
2726
  citation: [],
2592
- content: []
2727
+ content: [],
2728
+ aeo: []
2593
2729
  };
2594
2730
  for (const cat of Object.keys(report.categories)) {
2595
2731
  for (const r of report.categories[cat].results) {