gsc-cli 2.0.2 → 2.2.0

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/AUTH.md +205 -0
  3. data/FUNDING.md +120 -0
  4. data/README.md +463 -299
  5. data/bin/gsc +29158 -4921
  6. data/dist/gsc +29158 -4921
  7. data/lib/gsc/aio_hunter.rb +343 -0
  8. data/lib/gsc/answer_synthesizer.rb +157 -0
  9. data/lib/gsc/api.rb +53 -1
  10. data/lib/gsc/auth.rb +26 -0
  11. data/lib/gsc/backlinks_manager.rb +96 -0
  12. data/lib/gsc/brand_segmenter.rb +140 -0
  13. data/lib/gsc/cache_manager.rb +806 -0
  14. data/lib/gsc/cannibalization_analyzer.rb +141 -0
  15. data/lib/gsc/canonical_chains.rb +367 -0
  16. data/lib/gsc/citation_simulator.rb +339 -0
  17. data/lib/gsc/cli/aio_hunter.rb +154 -0
  18. data/lib/gsc/cli/analytics.rb +788 -0
  19. data/lib/gsc/cli/audit.rb +1976 -0
  20. data/lib/gsc/cli/base.rb +384 -0
  21. data/lib/gsc/cli/cache.rb +266 -0
  22. data/lib/gsc/cli/canonical.rb +223 -0
  23. data/lib/gsc/cli/citation_simulator.rb +152 -0
  24. data/lib/gsc/cli/dashboard.rb +354 -0
  25. data/lib/gsc/cli/doctor.rb +129 -0
  26. data/lib/gsc/cli/eeat.rb +125 -0
  27. data/lib/gsc/cli/ga4.rb +852 -0
  28. data/lib/gsc/cli/growth.rb +650 -0
  29. data/lib/gsc/cli/hreflang.rb +164 -0
  30. data/lib/gsc/cli/image_seo.rb +162 -0
  31. data/lib/gsc/cli/indexing.rb +458 -0
  32. data/lib/gsc/cli/intent_shift.rb +125 -0
  33. data/lib/gsc/cli/keyword_value.rb +134 -0
  34. data/lib/gsc/cli/keywords.rb +795 -0
  35. data/lib/gsc/cli/landing_roi.rb +308 -0
  36. data/lib/gsc/cli/low_ctr.rb +213 -0
  37. data/lib/gsc/cli/mobile_parity.rb +150 -0
  38. data/lib/gsc/cli/report.rb +100 -0
  39. data/lib/gsc/cli/rich_results.rb +172 -0
  40. data/lib/gsc/cli/schema_generate.rb +149 -0
  41. data/lib/gsc/cli/seasonal.rb +232 -0
  42. data/lib/gsc/cli/security.rb +153 -0
  43. data/lib/gsc/cli/setup.rb +1291 -0
  44. data/lib/gsc/cli/sitemap_tree.rb +143 -0
  45. data/lib/gsc/cli/skill_pack.rb +62 -0
  46. data/lib/gsc/cli/soft_404.rb +199 -0
  47. data/lib/gsc/cli/sparkline.rb +227 -0
  48. data/lib/gsc/cli/watchdog.rb +150 -0
  49. data/lib/gsc/cli/zombie_purger.rb +208 -0
  50. data/lib/gsc/cli.rb +706 -5111
  51. data/lib/gsc/cli_advanced.rb +1513 -0
  52. data/lib/gsc/client.rb +17 -2
  53. data/lib/gsc/color.rb +16 -1
  54. data/lib/gsc/command_registry.rb +47 -9
  55. data/lib/gsc/config.rb +11 -2
  56. data/lib/gsc/content_gap.rb +112 -0
  57. data/lib/gsc/ctr_curve.rb +115 -0
  58. data/lib/gsc/decay_predictor.rb +322 -0
  59. data/lib/gsc/doctor.rb +434 -0
  60. data/lib/gsc/eeat_auditor.rb +428 -0
  61. data/lib/gsc/entity_auditor.rb +229 -0
  62. data/lib/gsc/firewall_scanner.rb +733 -0
  63. data/lib/gsc/geo_auditor.rb +368 -0
  64. data/lib/gsc/google_suggest.rb +109 -0
  65. data/lib/gsc/google_trends.rb +8 -1
  66. data/lib/gsc/heading_validator.rb +283 -0
  67. data/lib/gsc/hreflang_validator.rb +412 -0
  68. data/lib/gsc/image_seo.rb +286 -0
  69. data/lib/gsc/indexing_queue.rb +179 -0
  70. data/lib/gsc/indexnow.rb +93 -0
  71. data/lib/gsc/intent_shift.rb +188 -0
  72. data/lib/gsc/internal_links.rb +249 -0
  73. data/lib/gsc/keyword_value.rb +191 -0
  74. data/lib/gsc/landing_roi.rb +195 -0
  75. data/lib/gsc/llms_generator.rb +425 -0
  76. data/lib/gsc/low_ctr_rewriter.rb +408 -0
  77. data/lib/gsc/mobile_parity.rb +222 -0
  78. data/lib/gsc/network_tracer.rb +93 -0
  79. data/lib/gsc/open_page_rank.rb +72 -0
  80. data/lib/gsc/page_analyzer.rb +47 -7
  81. data/lib/gsc/page_comparator.rb +108 -0
  82. data/lib/gsc/page_speed.rb +110 -0
  83. data/lib/gsc/prompts.rb +38 -29
  84. data/lib/gsc/questions_harvester.rb +178 -0
  85. data/lib/gsc/report_generator.rb +461 -0
  86. data/lib/gsc/rich_results.rb +388 -0
  87. data/lib/gsc/robots_checker.rb +114 -0
  88. data/lib/gsc/schema_generator.rb +788 -0
  89. data/lib/gsc/schema_validator.rb +120 -0
  90. data/lib/gsc/seasonal_predictor.rb +381 -0
  91. data/lib/gsc/security_scanner.rb +496 -0
  92. data/lib/gsc/serp_feature_detector.rb +359 -0
  93. data/lib/gsc/serp_preview.rb +152 -0
  94. data/lib/gsc/site_crawler.rb +113 -21
  95. data/lib/gsc/sitemap_loader.rb +15 -4
  96. data/lib/gsc/sitemap_tree.rb +301 -0
  97. data/lib/gsc/skill_pack.rb +195 -0
  98. data/lib/gsc/soft_404_analyzer.rb +385 -0
  99. data/lib/gsc/sparkline.rb +171 -0
  100. data/lib/gsc/speed_correlator.rb +416 -0
  101. data/lib/gsc/striking_playbook.rb +190 -0
  102. data/lib/gsc/title_optimizer.rb +420 -0
  103. data/lib/gsc/vault.rb +260 -0
  104. data/lib/gsc/version.rb +1 -1
  105. data/lib/gsc/watchdog.rb +235 -0
  106. data/lib/gsc/zombie_purger.rb +366 -0
  107. data/lib/gsc.rb +144 -0
  108. metadata +91 -2
data/lib/gsc/client.rb CHANGED
@@ -55,12 +55,27 @@ module GSC
55
55
  # Transparent gzip decompression
56
56
  raw_body = response.body
57
57
  body_str = if response['content-encoding'] =~ /gzip/i && raw_body && !raw_body.empty?
58
- Zlib::GzipReader.new(StringIO.new(raw_body)).read
58
+ begin
59
+ Zlib::GzipReader.new(StringIO.new(raw_body)).read
60
+ rescue Zlib::GzipFile::Error, Zlib::Error
61
+ raw_body
62
+ end
59
63
  else
60
64
  raw_body
61
65
  end
62
66
 
63
- body = body_str ? (JSON.parse(body_str) rescue body_str) : nil
67
+ body_str = body_str.to_s.dup.force_encoding('UTF-8').scrub if body_str
68
+
69
+ body = if body_str && !body_str.strip.empty?
70
+ begin
71
+ parsed = JSON.parse(body_str)
72
+ parsed.is_a?(Hash) || parsed.is_a?(Array) ? parsed : { 'value' => parsed }
73
+ rescue JSON::ParserError, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
74
+ { 'error' => { 'message' => body_str }, 'raw' => body_str }
75
+ end
76
+ else
77
+ {}
78
+ end
64
79
 
65
80
  {
66
81
  ok: response.is_a?(Net::HTTPSuccess),
data/lib/gsc/color.rb CHANGED
@@ -16,7 +16,22 @@ module GSC
16
16
  GRAY = "\e[90m"
17
17
 
18
18
  def self.c(text, *styles)
19
- "#{styles.join}#{text}#{RESET}"
19
+ t = text.to_s.dup.force_encoding('UTF-8').scrub
20
+ "#{styles.join}#{t}#{RESET}"
21
+ end
22
+
23
+ def self.cyan(str); c(str, CYAN); end
24
+ def self.yellow(str); c(str, YELLOW); end
25
+ def self.green(str); c(str, GREEN); end
26
+ def self.red(str); c(str, RED); end
27
+ def self.bold(str); c(str, BOLD); end
28
+ def self.gray(str); c(str, GRAY); end
29
+ def self.magenta(str); c(str, MAGENTA); end
30
+ def self.white(str); c(str, WHITE); end
31
+ def self.dim(str); c(str, DIM); end
32
+
33
+ def self.strip_ansi(str)
34
+ str.to_s.gsub(/\e\[[0-9;]*[mK]/, '')
20
35
  end
21
36
  end
22
37
  end
@@ -6,9 +6,9 @@ COMMAND_REGISTRY = [
6
6
  {
7
7
  category: "Google Trends & Keyword Demand",
8
8
  commands: [
9
- { name: "ke <seed|file>", shortcut: "ke", desc: "Keywords Everywhere: Exact monthly volume, CPC, competition & GSC correlation", flags: ["--country", "--limit", "--json"] },
10
- { name: "ke-credits", shortcut: "ke-credits", desc: "Check remaining Keywords Everywhere account API credits", flags: ["--json"] },
11
- { name: "connect ke [key]", shortcut: "connect ke", desc: "Connect Keywords Everywhere API key and save to config.json", flags: [] },
9
+ { name: "ke <seed|file>", shortcut: "ke", desc: "Keywords Everywhere: Exact monthly volume, CPC, competition & GSC correlation", flags: ["--country", "--limit", "--json"] },
10
+ { name: "ke-credits", shortcut: "ke-credits", desc: "Check remaining Keywords Everywhere account API credits", flags: ["--json"] },
11
+ { name: "connect ke [key]", shortcut: "connect ke", desc: "Connect Keywords Everywhere API key and save to config.json", flags: [] },
12
12
  { name: "trends <query>", shortcut: "tr", desc: "Live Google Trends: 5yr/1yr demand trajectory, velocity & breakout queries", flags: ["--geo", "--time", "--json"] },
13
13
  { name: "planner <seed>", shortcut: "kp", desc: "Free keyword planner: intent expansion & GSC ranking correlation", flags: ["--country", "--limit", "--json"] },
14
14
  { name: "planner-import <file>", shortcut: "pi", desc: "Import Google Ads CSV or Markdown table to score & rank opportunities", flags: ["--limit", "--json"] }
@@ -18,12 +18,18 @@ COMMAND_REGISTRY = [
18
18
  category: "Search Console Intelligence",
19
19
  commands: [
20
20
  { name: "performance", shortcut: "p", desc: "Executive dashboard: Clicks, Impressions, CTR, Position", flags: ["--days", "--limit", "--csv"] },
21
- { name: "top-queries", shortcut: "t", desc: "Top search queries, rankings, and CTR", flags: ["--days", "--limit", "-s", "--order", "--csv"] },
21
+ { name: "top-queries", shortcut: "t", desc: "Top search queries, rankings, and CTR", flags: ["--days", "--limit", "-s", "--order", "--brand", "--non-brand", "--brand-name", "--csv"] },
22
+ { name: "brand", shortcut: "b", desc: "Brand vs. non-brand search query segmentation & share", flags: ["--days", "--limit", "--brand-name", "--json"] },
23
+ { name: "ctr-curve", shortcut: nil, desc: "Expected SERP CTR curve & click gain simulator", flags: ["--target-pos", "--min-imp", "--limit", "--days", "--csv"] },
22
24
  { name: "top-pages", shortcut: nil, desc: "Top landing pages driving organic search clicks", flags: ["--days", "--limit", "-s", "--order", "--csv"] },
23
25
  { name: "opportunities", shortcut: "o", desc: "Striking-distance queries (Pos 7-20) to push to Top 3", flags: ["--days", "--min-imp", "--min-pos", "--max-pos"] },
26
+ { name: "strike", shortcut: "sp", desc: "Striking-distance tactical playbook: Title rewrites, heading recipes & internal link anchors (Pos 7-20)", flags: ["--days", "--min-imp", "--min-pos", "--max-pos", "--limit", "--csv", "--json"] },
24
27
  { name: "underperformers", shortcut: "u", desc: "High-ranking queries (Top 10) with low CTR (title tag wins)", flags: ["--days", "--min-imp"] },
28
+ { name: "low-ctr [query/url]", shortcut: "lc", desc: "Low-CTR High-Impression Title Tag Rewriter: 3 SERP-safe hook titles (<560px), click hemorrhage & revenue recovery", flags: ["--min-imp", "--cpc", "--limit", "--brand", "--csv", "--json"] },
25
29
  { name: "cannibalization", shortcut: "c", desc: "Detect internal URLs competing for the same keywords", flags: ["--days", "--min-imp"] },
30
+ { name: "questions-harvest [page]", shortcut: "qh", desc: "Harvest question queries from GSC and generate FAQPage JSON-LD schema", flags: ["--days", "--min-imp", "--limit", "--generate", "--csv", "--json"] },
26
31
  { name: "decay", shortcut: "d", desc: "Period-over-period decay detection (decaying vs surging)", flags: ["--compare"] },
32
+ { name: "sparklines [query|page]", shortcut: "spark", desc: "Terminal ASCII Sparklines & Multi-Horizon Trajectory Graphs (Clicks, Imp, CTR, Pos)", flags: ["--days", "--metric", "--height", "--data", "--csv", "--json"] },
27
33
  { name: "trends", shortcut: nil, desc: "Compare current vs prior period search trends", flags: ["--compare"] },
28
34
  { name: "devices", shortcut: nil, desc: "Desktop vs Mobile vs Tablet search traffic share", flags: ["--days"] },
29
35
  { name: "countries", shortcut: nil, desc: "Geographic search demand by country", flags: ["--days", "--limit"] },
@@ -52,27 +58,59 @@ COMMAND_REGISTRY = [
52
58
  commands: [
53
59
  { name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
54
60
  { name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
61
+ { name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--json"] },
55
62
  { name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
56
63
  { name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
57
64
  { name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
58
65
  { name: "sitemaps-submit <url>", shortcut: nil, desc: "Submit/register XML sitemap with Search Console", flags: ["-d"] },
59
66
  { name: "index-sitemap <file/url>", shortcut: nil, desc: "Batch notify Googlebot to index all sitemap URLs", flags: ["--delay", "--dry-run"] },
60
- { name: "inspect-sitemap <file/url>", shortcut: nil, desc: "Bulk inspect indexation status for all sitemap URLs", flags: ["--delay"] }
67
+ { name: "inspect-sitemap <file/url>", shortcut: nil, desc: "Bulk inspect indexation status for all sitemap URLs", flags: ["--delay"] },
68
+ { name: "indexnow <url>", shortcut: "in", desc: "Instant indexing via IndexNow protocol (Bing, Yandex, Seznam, Naver)", flags: ["--key", "--json"] },
69
+ { name: "indexnow-sitemap <sitemap>", shortcut: "ins", desc: "Batch submit all sitemap URLs to IndexNow", flags: ["--key", "--limit", "--json"] },
70
+ { name: "connect indexnow [key]", shortcut: nil, desc: "Configure IndexNow API key and display verification file details", flags: [] }
61
71
  ]
62
72
  },
63
73
  {
64
74
  category: "Health, Diagnostics & Setup",
65
75
  commands: [
76
+ { name: "geo [url]", shortcut: "aeo", desc: "Generative Engine Optimization: Audit AI bot access & citability score (ChatGPT, Perplexity, Claude)", flags: ["--json"] },
77
+ { name: "cite-sim <url|file> [query]", shortcut: "cite", desc: "AI Search Citation Simulator: Emulates ChatGPT, Perplexity & Claude citations, verifiable quote extraction & CLS score", flags: ["--query", "--model", "--csv", "--json"] },
78
+ { name: "answer <query>", shortcut: "ans", desc: "Synthesize high-citability 40-60w direct answers & info-gain snippets (Featured Snippets & Perplexity)", flags: ["--brand", "--type", "--generate", "--json"] },
79
+ { name: "entity [url]", shortcut: "kg", desc: "Knowledge Graph entity & sameAs disambiguation audit (Wikidata, Wikipedia, LinkedIn)", flags: ["--generate", "--json"] },
80
+ { name: "serp <query|url>", shortcut: "preview", desc: "Live SERP Feature Detector (AI Overviews, Snippets, PAA, Local) & Pixel Width Simulator", flags: ["--live", "--preview", "--title", "--desc", "--url", "--json"] },
81
+ { name: "serp-features <query>", shortcut: "sf", desc: "Scan live Google SERP features, Zero-Click Threat index & Rich Snippet capture playbook", flags: ["--json"] },
66
82
  { name: "page <url|file>", shortcut: "pg", desc: "Detailed On-Page DOM + Off-Page GSC Performance Audit", flags: ["--check-links", "--json"] },
83
+ { name: "headings <url|file>", shortcut: "ht", desc: "Heading hierarchy tree validator: H1-H6 nesting, depth, empty tags & health score", flags: ["--keyword", "--json"] },
67
84
  { name: "site-audit [sitemap]", shortcut: "crl", desc: "Full site crawl, broken links (404/500), image alts & AI report", flags: ["--check-links", "--report", "--json"] },
85
+ { name: "orphans [sitemap|url]", shortcut: "il", desc: "Internal link equity & orphan page rescue: Detect 0-link pages, click depth & suggested rescue sources", flags: ["--limit", "--concurrency", "--csv", "--json"] },
86
+ { name: "speed-correlate [url]", shortcut: "cwv", desc: "Correlate Core Web Vitals (LCP/CLS) directly with Search Console impressions & projected traffic surge", flags: ["--days", "--strategy", "-d", "--json"] },
87
+ { name: "firewall [url]", shortcut: "ai-bots", desc: "Scan /robots.txt & edge WAF/Cloudflare bot challenges for ChatGPT, Claude & Perplexity", flags: ["--user-agent", "--json"] },
88
+ { name: "security [url]", shortcut: "sec", desc: "Security Headers & Mixed Content Auditor: HSTS preload, CSP, clickjacking & SSL/TLS certificate expiry", flags: ["--json"] },
89
+ { name: "landing-roi [url]", shortcut: "roi", desc: "Landing Page Economic ROI & Revenue Hemorrhage: Merge GSC clicks with GA4 bounce rates to calculate dollar revenue leaks", flags: ["--aov", "--conv-rate", "--benchmark", "--clicks", "--bounce", "--csv", "--json"] },
90
+ { name: "schema-generate [url]", shortcut: "schema-gen", desc: "Structured Data Rich Snippet Generator: Auto-synthesizes valid Schema.org JSON-LD (Product, FAQ, HowTo, Article, Software) with Google validation", flags: ["--type", "--price", "--currency", "--rating", "--format", "--open", "--json"] },
91
+ { name: "sitemap-tree [path|url]", shortcut: "smt", desc: "Sitemap Index Hierarchy & Coverage Auditor: Recursive sub-sitemap tree, Google 50k URL / 50MB compliance & GSC coverage ratio", flags: ["--csv", "--json"] },
92
+ { name: "titles [sitemap|url]", shortcut: "title-opt", desc: "Batch audit title pixel width (>580px) & synthesize non-truncating alternatives", flags: ["--overflow-only", "--limit", "--concurrency", "--csv", "--json"] },
93
+ { name: "image-seo <url|file>", shortcut: "img", desc: "Image SEO & Next-Gen Format Auditor: WebP/AVIF formats, missing alt text, explicit width/height for CLS & LCP preloading", flags: ["--check-size", "--limit", "--csv", "--json"] },
94
+ { name: "hreflang-check <url|file>", shortcut: "hreflang", desc: "International Hreflang Reciprocity Validator: Bidirectional link graph, ISO 639-1 / 3166-1 codes & x-default fallback", flags: ["--no-reciprocity", "--csv", "--json"] },
95
+ { name: "eeat <url|file>", shortcut: "author", desc: "Author E-E-A-T & Credential Auditor: Person schema, credentials, editorial reviews, dates & Knowledge Graph links", flags: ["--csv", "--json"] },
68
96
  { name: "audit", shortcut: "a", desc: "360-degree Comprehensive SEO & GA4 health audit", flags: ["--days", "-d"] },
69
- { name: "zombies <sitemap>", shortcut: nil, desc: "Find zero-impression crawl waste pages over 90 days", flags: [] },
70
- { name: "use <domain or 1-9>", shortcut: nil, desc: "Switch active default domain", flags: [] },
97
+ { name: "report [domain]", shortcut: "rep", desc: "Automated Executive SEO & AI Search Report Generator: Single-file HTML dashboard & Markdown reports", flags: ["--html", "--md", "--days", "--title", "--json"] },
98
+ { name: "intent-shift [domain]", shortcut: "intent", desc: "Search Intent Drift & Landing Page Mismatch Tracker: Detect transactional/informational intent divergence & compute Portfolio Volatility Index", flags: ["--days", "--brand", "--csv", "--json"] },
99
+ { name: "rich-results <url|file>", shortcut: "rich-test", desc: "Google Rich Results Testing & Validation Suite: Disqualifying errors, recommended enhancements, 1-click JSON-LD fixes & browser testing", flags: ["--open", "--patch", "--csv", "--json"] },
100
+ { name: "watch [domain]", shortcut: "mon", desc: "Continuous SERP Watchdog & Rank Drift Monitor: Real-time rank drop alerts, CTR crash detection, webhooks & automated cron/launchd daemon", flags: ["--once", "--daemon", "--interval", "--threshold", "--webhook", "--slack", "--cron", "--launchd", "--systemd", "--csv", "--json"] },
101
+ { name: "kw-value [domain]", shortcut: "kwval", desc: "Conversion-Weighted Keyword Opportunity Matrix: Calculate revenue per keyword, simulate Top 3 pipeline upside & prioritize high-buyer-intent terms", flags: ["--aov", "--conv-rate", "--margin", "--target-pos", "--days", "--csv", "--json"] },
102
+ { name: "mobile-parity [domain]", shortcut: "mobile", desc: "Mobile vs. Desktop SERP Parity Auditor: Cross-device rank gaps, responsive suppression penalties & mobile CTR loss diagnosis", flags: ["--days", "--min-imp", "--gap-threshold", "--csv", "--json"] },
103
+ { name: "aio-hunter [query|domain]", shortcut: "aio", desc: "Google AI Overview (AIO) Opportunity Hunter: Detects AIO SERP presence, extracts cited sources, quantifies citation gaps & generates snippet capture recipes", flags: ["--limit", "--min-imp", "--days", "--csv", "--json"] },
104
+ { name: "skill-pack [dir]", shortcut: "agent-pack", desc: "Autonomous AI Agent Skill Packaging Engine: Synthesizes production SKILL.md, recipes.json & integration hooks for Antigravity, Claude Code & Cursor", flags: ["--target", "--dir", "--dry-run", "--json"] },
105
+ { name: "zombies [sitemap|file]", shortcut: "z", desc: "Zombie Content Purger & Crawl Equity Optimizer: CEDI score (0-100), automated 410/301 triage & 1-click server rules", flags: ["--days", "--threshold", "--action", "--format", "--inventory", "--csv", "--json"] },
106
+ { name: "soft-404 [url|file]", shortcut: "404", desc: "404 & Soft-404 Crawl Error Diagnostic: Detects thin/empty pages returning 200 OK, calculates crawl waste & synthesizes smart 301/410 redirect rules", flags: ["--concurrency", "--threshold", "--format", "--csv", "--json"] },
107
+ { name: "switch <domain>", shortcut: "use", desc: "Instant agency domain & credentials switcher (domain, alias, or index #)", flags: ["--json"] },
108
+ { name: "vault [action]", shortcut: nil, desc: "Agency Credential Vault: AES-256-GCM encrypted store for 50+ service accounts (list, add, remove, status)", flags: ["--domain", "--alias", "--property", "--json"] },
71
109
  { name: "domains", shortcut: nil, desc: "List verified domains and GA4 property links", flags: [] },
110
+ { name: "doctor", shortcut: "doc", desc: "Zero-Dependency Architecture Certification & System Health: Verifies 100% pure Ruby stdlib, cold-start latency (<100ms), OpenSSL cipher support & config permissions", flags: ["--strict", "--fix", "--json"] },
72
111
  { name: "where", shortcut: nil, desc: "Inspect installation path, active key, and config file", flags: [] },
73
112
  { name: "connect", shortcut: nil, desc: "1-Click Setup Wizard: auto-detects key or drag & drop", flags: [] },
74
113
  { name: "connect-ga4", shortcut: nil, desc: "Interactive GA4 linking wizard", flags: [] },
75
- { name: "open", shortcut: nil, desc: "Reveal configuration directory (~/.config/gsc) in Finder", flags: [] },
76
114
  { name: "prompts [id]", shortcut: "pb", desc: "25 Autonomous AI SEO Playbooks & ready-to-paste prompts", flags: ["--copy", "--json"] },
77
115
  { name: "skills [install|show]", shortcut: nil, desc: "Inspect or auto-install AI Agent Skill", flags: [] },
78
116
  { name: "commands", shortcut: nil, desc: "List all commands (human-readable or JSON with --json)", flags: ["--json"] }
@@ -267,7 +305,7 @@ COMMAND_REGISTRY = [
267
305
  2. **Optimal Length**: Keep `<title>` between **50 and 60 characters** (maximum 580px width) so Google does not truncate with `...`.
268
306
  3. **Emotional Hook / CTR Multiplier**: Include brackets `[Free Calculator]`, actionable numbers (`10 Best`, `2026 Checklist`), or primary value props.
269
307
  4. **Brand Suffix**: Always append ` | BrandName` at the end.
270
- 5. **Meta Description**: 130–155 characters summarizing the page benefit with a clear call-to-action (e.g. *"Calculate exact moving box counts by room, size, and weight. Free instant estimator."*).
308
+ 5. **Meta Description**: 130–155 characters summarizing the page benefit with a clear call-to-action (e.g. *"Inspect technical SEO health, Core Web Vitals, and index coverage instantly. Free diagnostic audit."*).
271
309
  6. **Heading Polish**: Ensure `<h1>` matches search intent and **never ends with a period (`.`)**.
272
310
 
273
311
  #### Step 4: Immediately Trigger Googlebot Indexing
data/lib/gsc/config.rb CHANGED
@@ -9,6 +9,15 @@ module GSC
9
9
  CONFIG_DIR = File.expand_path('~/.config/gsc')
10
10
  CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
11
11
 
12
+ def self.get(key)
13
+ load[key.to_s]
14
+ end
15
+
16
+ def self.set(key, value)
17
+ save(key.to_s => value)
18
+ value
19
+ end
20
+
12
21
  def self.load
13
22
  return {} unless File.exist?(CONFIG_FILE)
14
23
  JSON.parse(File.read(CONFIG_FILE))
@@ -144,7 +153,7 @@ def self.load_saved_keywords(domain, identifier)
144
153
 
145
154
  target_file = if identifier.to_s =~ /^\d+$/
146
155
  idx = identifier.to_i - 1
147
- list[idx]&.fetch('path', nil)
156
+ idx >= 0 ? list[idx]&.fetch('path', nil) : nil
148
157
  else
149
158
  found = list.find { |item| item['file'] == identifier || item['seed'] == identifier || item['file'].include?(identifier.to_s) }
150
159
  found ? found['path'] : nil
@@ -162,7 +171,7 @@ def self.delete_saved_keywords(domain, identifier)
162
171
 
163
172
  target_file = if identifier.to_s =~ /^\d+$/
164
173
  idx = identifier.to_i - 1
165
- list[idx]&.fetch('path', nil)
174
+ idx >= 0 ? list[idx]&.fetch('path', nil) : nil
166
175
  else
167
176
  found = list.find { |item| item['file'] == identifier || item['file'].include?(identifier.to_s) }
168
177
  found ? found['path'] : nil
@@ -0,0 +1,112 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'set'
5
+
6
+ module GSC
7
+ class ContentGap
8
+ STOP_WORDS = Set.new(%w[
9
+ a about above after again against all am an and any are aren't as at be because been before being below
10
+ between both but by can't cannot could couldn't did didn't do does doesn't doing don't down during each
11
+ few for from further had hadn't has hasn't have haven't having he he'd he'll he's her here here's hers
12
+ herself him himself his how how's i i'd i'll i'm i've if in into is isn't it it's its itself let's me
13
+ more most mustn't my myself no nor not of off on once only or other ought our ours ourselves out over own
14
+ same shan't she she'd she'll she's should shouldn't so some such than that that's the their theirs them
15
+ themselves then there there's these they they'd they'll they're they've this those through to too under until
16
+ up very was wasn't we we'd we'll we're we've were weren't what what's when when's where where's which while
17
+ who who's whom why why's with won't would wouldn't you you'd you'll you're you've your yours yourself yourselves
18
+ ])
19
+
20
+ attr_reader :url1, :url2
21
+
22
+ def initialize(url1, url2)
23
+ @url1 = url1
24
+ @url2 = url2
25
+ end
26
+
27
+ def analyze
28
+ pa1 = GSC::PageAnalyzer.new(@url1)
29
+ pa2 = GSC::PageAnalyzer.new(@url2)
30
+
31
+ data1 = pa1.fetch_and_analyze
32
+ data2 = pa2.fetch_and_analyze
33
+
34
+ text1 = extract_clean_text(pa1.html || '')
35
+ text2 = extract_clean_text(pa2.html || '')
36
+
37
+ tokens1 = tokenize(text1)
38
+ tokens2 = tokenize(text2)
39
+
40
+ unigrams1 = ngrams(tokens1, 1)
41
+ unigrams2 = ngrams(tokens2, 1)
42
+
43
+ bigrams1 = ngrams(tokens1, 2)
44
+ bigrams2 = ngrams(tokens2, 2)
45
+
46
+ trigrams1 = ngrams(tokens1, 3)
47
+ trigrams2 = ngrams(tokens2, 3)
48
+
49
+ # Competitor terms that occur at least 2 times, but occur 0 times in page1
50
+ missing_unigrams = term_gap(unigrams1, unigrams2, min_count: 2)
51
+ missing_bigrams = term_gap(bigrams1, bigrams2, min_count: 2)
52
+ missing_trigrams = term_gap(trigrams1, trigrams2, min_count: 2)
53
+
54
+ # Heading gaps (H2/H3 in page2 that have no match in page1)
55
+ h1 = (data1.dig(:headings, :h2) || []) + (data1.dig(:headings, :h3) || [])
56
+ h2 = (data2.dig(:headings, :h2) || []) + (data2.dig(:headings, :h3) || [])
57
+
58
+ missing_headings = h2.reject do |heading|
59
+ h1.any? { |my_h| my_h.downcase.include?(heading.downcase[0..15]) }
60
+ end
61
+
62
+ {
63
+ page1: { url: @url1, word_count: tokens1.length },
64
+ page2: { url: @url2, word_count: tokens2.length },
65
+ missing_unigrams: missing_unigrams.first(15),
66
+ missing_bigrams: missing_bigrams.first(15),
67
+ missing_trigrams: missing_trigrams.first(10),
68
+ missing_headings: missing_headings.first(10)
69
+ }
70
+ end
71
+
72
+ private
73
+
74
+ def extract_clean_text(html)
75
+ text = html.dup
76
+ text.gsub!(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/i, ' ')
77
+ text.gsub!(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/i, ' ')
78
+ text.gsub!(/<nav\b[^<]*(?:(?!<\/nav>)<[^<]*)*<\/nav>/i, ' ')
79
+ text.gsub!(/<footer\b[^<]*(?:(?!<\/footer>)<[^<]*)*<\/footer>/i, ' ')
80
+ text.gsub!(/<[^>]+>/, ' ')
81
+ text.gsub!(/&[a-z]+;/i, ' ')
82
+ text.gsub!(/\s+/, ' ')
83
+ text.strip
84
+ end
85
+
86
+ def tokenize(text)
87
+ text.downcase.scan(/[a-z0-9]+/).reject { |w| w.length < 3 || STOP_WORDS.include?(w) }
88
+ end
89
+
90
+ def ngrams(tokens, n)
91
+ counts = Hash.new(0)
92
+ return counts if tokens.length < n
93
+
94
+ (0..(tokens.length - n)).each do |i|
95
+ gram = tokens[i, n].join(' ')
96
+ counts[gram] += 1
97
+ end
98
+ counts
99
+ end
100
+
101
+ def term_gap(counts1, counts2, min_count: 2)
102
+ gap = []
103
+ counts2.each do |term, count2|
104
+ count1 = counts1[term] || 0
105
+ if count2 >= min_count && count1 == 0
106
+ gap << { term: term, competitor_count: count2, your_count: count1 }
107
+ end
108
+ end
109
+ gap.sort_by { |item| -item[:competitor_count] }
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GSC
4
+ class CtrCurve
5
+ # Empirical Google Organic SERP CTR Curve benchmark (Ahrefs / Backlinko / Advanced Web Ranking composite)
6
+ BENCHMARK_CTR = {
7
+ 1 => 28.0,
8
+ 2 => 15.5,
9
+ 3 => 11.0,
10
+ 4 => 8.0,
11
+ 5 => 6.0,
12
+ 6 => 4.5,
13
+ 7 => 3.5,
14
+ 8 => 2.8,
15
+ 9 => 2.2,
16
+ 10 => 1.8,
17
+ 11 => 1.4,
18
+ 12 => 1.2,
19
+ 13 => 1.0,
20
+ 14 => 0.9,
21
+ 15 => 0.8,
22
+ 16 => 0.7,
23
+ 17 => 0.6,
24
+ 18 => 0.5,
25
+ 19 => 0.4,
26
+ 20 => 0.4
27
+ }.freeze
28
+
29
+ DEFAULT_PAGE2_CTR = 0.5
30
+ DEFAULT_PAGE3_CTR = 0.2
31
+
32
+ def self.benchmark_for(position)
33
+ pos = position.to_f.round
34
+ return BENCHMARK_CTR[1] if pos <= 1
35
+ return BENCHMARK_CTR[pos] if BENCHMARK_CTR.key?(pos)
36
+ return DEFAULT_PAGE2_CTR if pos <= 20
37
+
38
+ DEFAULT_PAGE3_CTR
39
+ end
40
+
41
+ def self.project(rows, target_pos: 3, min_imp: 10)
42
+ target_pos = target_pos.to_i
43
+ target_pos = 3 if target_pos <= 0
44
+ target_ctr = benchmark_for(target_pos)
45
+
46
+ analyzed = []
47
+ total_incremental = 0
48
+ underperforming_queries = []
49
+
50
+ rows.each do |row|
51
+ imp = row[:impressions] || row['impressions'] || 0
52
+ next if imp < min_imp
53
+
54
+ clicks = row[:clicks] || row['clicks'] || 0
55
+ actual_ctr = if imp > 0
56
+ (clicks.to_f / imp * 100.0).round(2)
57
+ elsif (row[:ctr] || row['ctr'])
58
+ (row[:ctr] || row['ctr']).to_f.round(2)
59
+ else
60
+ 0.0
61
+ end
62
+ pos = (row[:position] || row['position'] || 100.0).to_f.round(1)
63
+ expected_ctr = benchmark_for(pos)
64
+
65
+ # Gain projection if ranking moved to target_pos
66
+ projected_clicks = ((target_ctr / 100.0) * imp).round
67
+ incremental_clicks = [projected_clicks - clicks, 0].max
68
+
69
+ # Priority score: Higher incremental clicks and closer to top page 1 = higher leverage
70
+ proximity_factor = [1.0, (25.0 - [pos, 25.0].min) / 20.0].max
71
+ opportunity_score = (incremental_clicks * proximity_factor).round(1)
72
+
73
+ # Flag title tag optimization wins: ranking on Page 1 (<=10) but CTR is < 50% of benchmark
74
+ is_underperforming = pos <= 10.0 && actual_ctr < (expected_ctr * 0.6) && imp >= 20
75
+
76
+ data = {
77
+ query: row[:query] || row['query'],
78
+ current_position: pos,
79
+ target_position: target_pos,
80
+ impressions: imp,
81
+ current_clicks: clicks,
82
+ current_ctr: actual_ctr,
83
+ expected_ctr: expected_ctr,
84
+ target_ctr: target_ctr,
85
+ projected_clicks: projected_clicks,
86
+ simulated_clicks: projected_clicks, # alias for backward compatibility
87
+ incremental_clicks: incremental_clicks,
88
+ opportunity_score: opportunity_score,
89
+ underperforming: is_underperforming
90
+ }
91
+
92
+ analyzed << data
93
+ total_incremental += incremental_clicks
94
+ underperforming_queries << data if is_underperforming
95
+ end
96
+
97
+ # Sort by highest opportunity score
98
+ analyzed.sort_by! { |r| -r[:opportunity_score] }
99
+ underperforming_queries.sort_by! { |r| -r[:impressions] }
100
+
101
+ {
102
+ target_position: target_pos,
103
+ target_ctr: target_ctr,
104
+ total_queries_analyzed: analyzed.size,
105
+ total_incremental_clicks: total_incremental,
106
+ opportunities: analyzed,
107
+ underperformers: underperforming_queries
108
+ }
109
+ end
110
+
111
+ class << self
112
+ alias simulate project
113
+ end
114
+ end
115
+ end