gsc-cli 2.0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +500 -0
- data/bin/gsc +8128 -0
- data/dist/gsc +8128 -0
- data/lib/gsc/api.rb +300 -0
- data/lib/gsc/auth.rb +92 -0
- data/lib/gsc/cli.rb +5435 -0
- data/lib/gsc/client.rb +79 -0
- data/lib/gsc/color.rb +22 -0
- data/lib/gsc/command_registry.rb +335 -0
- data/lib/gsc/config.rb +178 -0
- data/lib/gsc/google_trends.rb +187 -0
- data/lib/gsc/keyword_planner.rb +256 -0
- data/lib/gsc/keywords_everywhere.rb +144 -0
- data/lib/gsc/page_analyzer.rb +431 -0
- data/lib/gsc/prompts.rb +285 -0
- data/lib/gsc/site_crawler.rb +260 -0
- data/lib/gsc/sitemap_loader.rb +91 -0
- data/lib/gsc/version.rb +5 -0
- data/lib/gsc.rb +38 -0
- metadata +67 -0
data/lib/gsc/client.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'zlib'
|
|
7
|
+
require 'stringio'
|
|
8
|
+
|
|
9
|
+
module GSC
|
|
10
|
+
class Client
|
|
11
|
+
def initialize(token:)
|
|
12
|
+
@token = token
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(url)
|
|
16
|
+
uri = URI(url)
|
|
17
|
+
req = Net::HTTP::Get.new(uri)
|
|
18
|
+
prepare_headers(req)
|
|
19
|
+
execute(uri, req)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def post(url, body)
|
|
23
|
+
uri = URI(url)
|
|
24
|
+
req = Net::HTTP::Post.new(uri)
|
|
25
|
+
prepare_headers(req)
|
|
26
|
+
req['Content-Type'] = 'application/json'
|
|
27
|
+
req.body = JSON.generate(body)
|
|
28
|
+
execute(uri, req)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def put(url)
|
|
32
|
+
uri = URI(url)
|
|
33
|
+
req = Net::HTTP::Put.new(uri)
|
|
34
|
+
prepare_headers(req)
|
|
35
|
+
execute(uri, req)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def prepare_headers(req)
|
|
41
|
+
req['Authorization'] = "Bearer #{@token}"
|
|
42
|
+
req['Accept-Encoding'] = 'gzip'
|
|
43
|
+
ver = defined?(GSC::VERSION) ? GSC::VERSION : '1.0.0'
|
|
44
|
+
req['User-Agent'] = "gsc-cli/#{ver} (gzip)"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def execute(uri, req)
|
|
48
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
49
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
50
|
+
http.open_timeout = 10
|
|
51
|
+
http.read_timeout = 30
|
|
52
|
+
|
|
53
|
+
response = http.request(req)
|
|
54
|
+
|
|
55
|
+
# Transparent gzip decompression
|
|
56
|
+
raw_body = response.body
|
|
57
|
+
body_str = if response['content-encoding'] =~ /gzip/i && raw_body && !raw_body.empty?
|
|
58
|
+
Zlib::GzipReader.new(StringIO.new(raw_body)).read
|
|
59
|
+
else
|
|
60
|
+
raw_body
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
body = body_str ? (JSON.parse(body_str) rescue body_str) : nil
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
ok: response.is_a?(Net::HTTPSuccess),
|
|
67
|
+
status: response.code.to_i,
|
|
68
|
+
data: body,
|
|
69
|
+
compressed: (response['content-encoding'] =~ /gzip/i ? true : false)
|
|
70
|
+
}
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
{
|
|
73
|
+
ok: false,
|
|
74
|
+
status: 0,
|
|
75
|
+
data: { 'error' => { 'message' => e.message } }
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/gsc/color.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GSC
|
|
4
|
+
module Color
|
|
5
|
+
RESET = "\e[0m"
|
|
6
|
+
BOLD = "\e[1m"
|
|
7
|
+
DIM = "\e[2m"
|
|
8
|
+
UNDERLINE = "\e[4m"
|
|
9
|
+
RED = "\e[31m"
|
|
10
|
+
GREEN = "\e[32m"
|
|
11
|
+
YELLOW = "\e[33m"
|
|
12
|
+
BLUE = "\e[34m"
|
|
13
|
+
MAGENTA = "\e[35m"
|
|
14
|
+
CYAN = "\e[36m"
|
|
15
|
+
WHITE = "\e[37m"
|
|
16
|
+
GRAY = "\e[90m"
|
|
17
|
+
|
|
18
|
+
def self.c(text, *styles)
|
|
19
|
+
"#{styles.join}#{text}#{RESET}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GSC
|
|
4
|
+
module CommandRegistry
|
|
5
|
+
COMMAND_REGISTRY = [
|
|
6
|
+
{
|
|
7
|
+
category: "Google Trends & Keyword Demand",
|
|
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: [] },
|
|
12
|
+
{ name: "trends <query>", shortcut: "tr", desc: "Live Google Trends: 5yr/1yr demand trajectory, velocity & breakout queries", flags: ["--geo", "--time", "--json"] },
|
|
13
|
+
{ name: "planner <seed>", shortcut: "kp", desc: "Free keyword planner: intent expansion & GSC ranking correlation", flags: ["--country", "--limit", "--json"] },
|
|
14
|
+
{ name: "planner-import <file>", shortcut: "pi", desc: "Import Google Ads CSV or Markdown table to score & rank opportunities", flags: ["--limit", "--json"] }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
category: "Search Console Intelligence",
|
|
19
|
+
commands: [
|
|
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"] },
|
|
22
|
+
{ name: "top-pages", shortcut: nil, desc: "Top landing pages driving organic search clicks", flags: ["--days", "--limit", "-s", "--order", "--csv"] },
|
|
23
|
+
{ name: "opportunities", shortcut: "o", desc: "Striking-distance queries (Pos 7-20) to push to Top 3", flags: ["--days", "--min-imp", "--min-pos", "--max-pos"] },
|
|
24
|
+
{ name: "underperformers", shortcut: "u", desc: "High-ranking queries (Top 10) with low CTR (title tag wins)", flags: ["--days", "--min-imp"] },
|
|
25
|
+
{ name: "cannibalization", shortcut: "c", desc: "Detect internal URLs competing for the same keywords", flags: ["--days", "--min-imp"] },
|
|
26
|
+
{ name: "decay", shortcut: "d", desc: "Period-over-period decay detection (decaying vs surging)", flags: ["--compare"] },
|
|
27
|
+
{ name: "trends", shortcut: nil, desc: "Compare current vs prior period search trends", flags: ["--compare"] },
|
|
28
|
+
{ name: "devices", shortcut: nil, desc: "Desktop vs Mobile vs Tablet search traffic share", flags: ["--days"] },
|
|
29
|
+
{ name: "countries", shortcut: nil, desc: "Geographic search demand by country", flags: ["--days", "--limit"] },
|
|
30
|
+
{ name: "cities", shortcut: nil, desc: "Top visitor cities and retention via GA4", flags: ["--days", "--limit"] },
|
|
31
|
+
{ name: "snippets", shortcut: nil, desc: "Search appearance and rich snippet results (Reviews, Products, FAQs)", flags: ["--days"] }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
category: "Google Analytics 4 (GA4)",
|
|
36
|
+
commands: [
|
|
37
|
+
{ name: "realtime", shortcut: "r", desc: "Stream live active visitors and active page paths", flags: ["--watch"] },
|
|
38
|
+
{ name: "pages", shortcut: nil, desc: "Top pages with views, users, and average engagement time", flags: ["--days", "--limit"] },
|
|
39
|
+
{ name: "sources", shortcut: nil, desc: "Traffic acquisition channels (Organic, Direct, Social, Paid)", flags: ["--days"] },
|
|
40
|
+
{ name: "geo", shortcut: nil, desc: "Country visitor distribution from GA4", flags: ["--days", "--limit"] },
|
|
41
|
+
{ name: "tech", shortcut: nil, desc: "Device category breakdown (Mobile, Desktop, Tablet)", flags: ["--days"] },
|
|
42
|
+
{ name: "conversions", shortcut: nil, desc: "Key event / conversion goal performance", flags: ["--days"] },
|
|
43
|
+
{ name: "search-terms", shortcut: nil, desc: "Internal site search query tracking", flags: ["--days"] },
|
|
44
|
+
{ name: "ga4", shortcut: nil, desc: "Landing page bounce rates, sessions, duration", flags: ["--organic", "--site-only", "--all-hosts"] },
|
|
45
|
+
{ name: "correlation", shortcut: nil, desc: "Merge GSC keyword rankings with GA4 bounce rates", flags: ["--organic", "--site-only", "--all-hosts"] },
|
|
46
|
+
{ name: "ads", shortcut: nil, desc: "Google Ads campaign performance (Clicks, Cost, CPC, Conversions)", flags: ["--days"] },
|
|
47
|
+
{ name: "ga4-properties", shortcut: nil, desc: "Discover all GA4 properties accessible by service account", flags: [] }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
category: "Google Indexing API & Sitemaps",
|
|
52
|
+
commands: [
|
|
53
|
+
{ name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
|
|
54
|
+
{ name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
|
|
55
|
+
{ name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
|
|
56
|
+
{ name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
|
|
57
|
+
{ name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
|
|
58
|
+
{ name: "sitemaps-submit <url>", shortcut: nil, desc: "Submit/register XML sitemap with Search Console", flags: ["-d"] },
|
|
59
|
+
{ 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"] }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
category: "Health, Diagnostics & Setup",
|
|
65
|
+
commands: [
|
|
66
|
+
{ name: "page <url|file>", shortcut: "pg", desc: "Detailed On-Page DOM + Off-Page GSC Performance Audit", flags: ["--check-links", "--json"] },
|
|
67
|
+
{ name: "site-audit [sitemap]", shortcut: "crl", desc: "Full site crawl, broken links (404/500), image alts & AI report", flags: ["--check-links", "--report", "--json"] },
|
|
68
|
+
{ 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: [] },
|
|
71
|
+
{ name: "domains", shortcut: nil, desc: "List verified domains and GA4 property links", flags: [] },
|
|
72
|
+
{ name: "where", shortcut: nil, desc: "Inspect installation path, active key, and config file", flags: [] },
|
|
73
|
+
{ name: "connect", shortcut: nil, desc: "1-Click Setup Wizard: auto-detects key or drag & drop", flags: [] },
|
|
74
|
+
{ 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
|
+
{ name: "prompts [id]", shortcut: "pb", desc: "25 Autonomous AI SEO Playbooks & ready-to-paste prompts", flags: ["--copy", "--json"] },
|
|
77
|
+
{ name: "skills [install|show]", shortcut: nil, desc: "Inspect or auto-install AI Agent Skill", flags: [] },
|
|
78
|
+
{ name: "commands", shortcut: nil, desc: "List all commands (human-readable or JSON with --json)", flags: ["--json"] }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
].freeze
|
|
82
|
+
|
|
83
|
+
SKILL_MD_CONTENT = <<~SKILL
|
|
84
|
+
---
|
|
85
|
+
name: gsc
|
|
86
|
+
description: Automates Google Search Console, Google Indexing API, live URL index inspection, sitemap batch submission, and search ranking analytics (queries, impressions, positions, CTR). Trigger whenever checking SEO rankings, inspecting Google indexing status, analyzing search impressions/clicks, or notifying Googlebot of new/updated pages.
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# Google Search Console & Indexing API (GSC CLI) Agent Skill
|
|
90
|
+
|
|
91
|
+
This skill allows AI agents (Google Antigravity, Claude Code, Cursor, Codex, etc.) to programmatically interact with Google Search Console and the Google Indexing API using the `gsc` CLI tool.
|
|
92
|
+
|
|
93
|
+
## Key Capabilities
|
|
94
|
+
- **Search Analytics**: Query real keyword rankings, impressions, clicks, CTR, and average SERP positions.
|
|
95
|
+
- **Growth Intelligence**: Uncover striking-distance Page 2 opportunities, CTR underperformers, and keyword cannibalization conflicts.
|
|
96
|
+
- **Trend & Decay Analysis**: 28-day period-over-period decay detection (decaying, surging, new, lost).
|
|
97
|
+
- **Crawl Optimization**: Scan sitemaps for 90-day zero-impression zombie pages wasting crawl budget.
|
|
98
|
+
- **Instant Googlebot Indexing**: Ping Google's Indexing API with `URL_UPDATED` or `URL_DELETED` to trigger immediate crawling.
|
|
99
|
+
- **Live URL Inspection**: Query Search Console API for actual index status (`PASS`, coverage state, canonical assigned by Google, crawl date).
|
|
100
|
+
- **Bulk Sitemap Audit**: Inspect entire XML sitemaps to identify indexed vs queued URLs.
|
|
101
|
+
- **GA4 Behavioral & Realtime**: Live visitor streaming, post-click bounce rates, engagement rates, and Google Ads ROI.
|
|
102
|
+
- **Multi-domain Management**: Easily view or switch active domains across projects.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Agent Rule: Always Use `--json` Flag
|
|
107
|
+
When running `gsc` commands from agent tools (`run_command`), **always append `--json`** to receive clean, machine-readable JSON output instead of ANSI terminal formatting:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
gsc top-queries --json
|
|
111
|
+
gsc opportunities --json
|
|
112
|
+
gsc inspect https://example.com/page --json
|
|
113
|
+
gsc performance --days 30 --json
|
|
114
|
+
gsc realtime --json
|
|
115
|
+
gsc ads --json
|
|
116
|
+
gsc channels --json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Common Agent Workflows
|
|
122
|
+
|
|
123
|
+
### 1. Analyzing Keyword Rankings & Opportunities
|
|
124
|
+
When the user asks about traffic, rankings, or keyword performance:
|
|
125
|
+
```bash
|
|
126
|
+
# Get top 20 queries for the active domain (sorted by impressions)
|
|
127
|
+
gsc top-queries -s imp --limit 20 --json
|
|
128
|
+
|
|
129
|
+
# Find highest-ranking queries (page 1 rankings, pos ascending)
|
|
130
|
+
gsc top-queries -s pos --limit 20 --json
|
|
131
|
+
|
|
132
|
+
# Find striking-distance opportunities (Page 2 keywords to push to Top 3)
|
|
133
|
+
gsc opportunities --min-imp 10 --json
|
|
134
|
+
|
|
135
|
+
# Find Top 10 queries with low CTR (easy title tag rewrites)
|
|
136
|
+
gsc underperformers --json
|
|
137
|
+
|
|
138
|
+
# Detect multiple URLs competing for the same search query
|
|
139
|
+
gsc cannibalization --json
|
|
140
|
+
|
|
141
|
+
# Detect traffic drops and ranking decay
|
|
142
|
+
gsc decay --json
|
|
143
|
+
```
|
|
144
|
+
**Interpretation Advice**:
|
|
145
|
+
- Sort queries by impressions to find high-volume search terms.
|
|
146
|
+
- Striking-distance keywords (position 7–20 with high impressions) are prime candidates for on-page content expansion to push them into Top 3.
|
|
147
|
+
- CTR underperformers already rank on Page 1—simply rewrite their `<title>` and meta description with emotional hooks or benefit promises to double traffic.
|
|
148
|
+
|
|
149
|
+
### 2. Multi-Dimensional Performance & Geographic Analytics
|
|
150
|
+
When the user asks for high-level performance, devices, country breakdown, or search appearances:
|
|
151
|
+
```bash
|
|
152
|
+
# Full 360° executive dashboard (Totals, Devices, Countries, Snippets, Queries, Pages, Cities)
|
|
153
|
+
gsc performance --days 30 --json
|
|
154
|
+
|
|
155
|
+
# Device breakdown (Desktop vs Mobile vs Tablet clicks share)
|
|
156
|
+
gsc devices --json
|
|
157
|
+
|
|
158
|
+
# Top countries driving organic search impressions & clicks
|
|
159
|
+
gsc countries --limit 20 --json
|
|
160
|
+
|
|
161
|
+
# Top visitor cities and on-site engagement (via GA4)
|
|
162
|
+
gsc cities --limit 20 --json
|
|
163
|
+
|
|
164
|
+
# Search appearance & rich snippets (Review stars, Product snippets, FAQ rich results)
|
|
165
|
+
gsc snippets --json
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 3. Inspecting Page Index Status
|
|
169
|
+
When the user asks if a specific URL is indexed or why it's not showing up on Google:
|
|
170
|
+
```bash
|
|
171
|
+
gsc inspect https://example.com/features/new-feature --json
|
|
172
|
+
```
|
|
173
|
+
**Interpreting the JSON Response**:
|
|
174
|
+
- `verdict: "PASS"`: The page is successfully indexed and eligible for search results.
|
|
175
|
+
- `coverageState: "Crawled - currently not indexed"`: Google crawled the page but decided not to index it (often due to thin content, duplicate canonical, or low internal linking).
|
|
176
|
+
- `coverageState: "Discovered - currently not indexed"`: Google knows about the URL (via sitemap or link) but has not yet crawled it. You can trigger `gsc index <url>` to accelerate crawling.
|
|
177
|
+
- `robotsTxtState: "DISALLOWED"`: The page is blocked by `robots.txt`.
|
|
178
|
+
|
|
179
|
+
### 3. Pinging Googlebot After Creating/Updating Content
|
|
180
|
+
Whenever you generate a new blog post, landing page, or programmatic SEO route, notify Googlebot immediately:
|
|
181
|
+
```bash
|
|
182
|
+
gsc index https://example.com/blog/new-post --json
|
|
183
|
+
```
|
|
184
|
+
*(If a page was permanently deleted, use `gsc remove <url> --json` to protect store crawl health).*
|
|
185
|
+
|
|
186
|
+
### 4. Auditing Sitemaps
|
|
187
|
+
To bulk check whether all URLs in a sitemap are actually indexed:
|
|
188
|
+
```bash
|
|
189
|
+
gsc inspect-sitemap https://example.com/sitemap.xml --delay 300 --json
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 5. Managing Active Domains & Credentials
|
|
193
|
+
```bash
|
|
194
|
+
# Check current environment & key
|
|
195
|
+
gsc where --json
|
|
196
|
+
|
|
197
|
+
# List all verified domain properties accessible by key
|
|
198
|
+
gsc domains --json
|
|
199
|
+
|
|
200
|
+
# Set or switch active domain globally
|
|
201
|
+
gsc use example.com --json
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 6. GA4 Behavioral, Realtime & Google Ads Analytics
|
|
205
|
+
```bash
|
|
206
|
+
# Stream live active visitors on site right now
|
|
207
|
+
gsc realtime --json
|
|
208
|
+
|
|
209
|
+
# Top landing pages with bounce rates, duration, and engagement
|
|
210
|
+
gsc ga4 --organic --json
|
|
211
|
+
|
|
212
|
+
# Correlate pre-click GSC search queries with post-click GA4 bounce rates
|
|
213
|
+
gsc correlation --json
|
|
214
|
+
|
|
215
|
+
# Google Ads campaign performance (clicks, cost, CPC, conversions, CPA)
|
|
216
|
+
gsc ads --json
|
|
217
|
+
|
|
218
|
+
# Omnichannel traffic sources breakdown (Organic, Paid, Direct, Referral)
|
|
219
|
+
gsc channels --json
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
### 7. Working with Saved Domain Keyword Libraries & Research
|
|
225
|
+
GSC CLI maintains persistent, domain-scoped keyword research libraries in `~/.config/gsc/domains/<domain>/keywords/`. AI agents can use these archives to track target keywords over time and measure ranking progress:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# List all saved research snapshots for the active domain
|
|
229
|
+
gsc saved --json
|
|
230
|
+
|
|
231
|
+
# Inspect keyword metrics (Volume, CPC, Competition Tier, Opportunity Score, 12m trends)
|
|
232
|
+
gsc saved view 1 --json
|
|
233
|
+
|
|
234
|
+
# Re-check an archived research snapshot against live Google Search Console rankings
|
|
235
|
+
gsc saved check 1 --json
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Agent Strategy for Saved Keywords**:
|
|
239
|
+
- When the user asks *"What keywords should we target next?"* or *"How are our target keywords performing?"*, run `gsc saved --json` and inspect recent archives with `gsc saved check 1 --json`.
|
|
240
|
+
- Identify keywords flagged as `🚀 Untargeted` (keywords with high search volume and high opportunity score where your domain currently has 0 impressions): these are prime candidates for **new landing pages, programmatic SEO templates, or blog posts**.
|
|
241
|
+
- Identify keywords in `🎯 Page 2 Striking Distance` (positions 7–20): these are prime candidates for **on-page content expansion, internal link additions, and title tag rewrites**.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### 8. Autonomous Title & Meta Description Optimization Loop
|
|
246
|
+
AI agents can execute a full, closed-loop title and CTR optimization cycle:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
[1. Identify Opportunities] ➔ [2. Inspect Current Code] ➔ [3. Rewrite Title & Meta] ➔ [4. Ping Googlebot]
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Step 1: Discover CTR Underperformers & Striking-Distance Keywords
|
|
253
|
+
```bash
|
|
254
|
+
# Find Page 1 queries with high impressions but below-average CTR
|
|
255
|
+
gsc underperformers --json
|
|
256
|
+
|
|
257
|
+
# Find striking-distance queries (positions 7-20) with high impressions
|
|
258
|
+
gsc opportunities --min-imp 20 --json
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### Step 2: Locate the Page in Codebase
|
|
262
|
+
Locate the corresponding template or page in the application repository (e.g. Rails views, Svelte pages, Next.js routes, or HTML templates).
|
|
263
|
+
|
|
264
|
+
#### Step 3: Apply the High-CTR Title & Description Formula
|
|
265
|
+
Rewrite `<title>` and `<meta name="description">` according to these strict rules:
|
|
266
|
+
1. **Front-Load the Exact Query**: Place the highest-impression search query in the first 30 characters of `<title>`.
|
|
267
|
+
2. **Optimal Length**: Keep `<title>` between **50 and 60 characters** (maximum 580px width) so Google does not truncate with `...`.
|
|
268
|
+
3. **Emotional Hook / CTR Multiplier**: Include brackets `[Free Calculator]`, actionable numbers (`10 Best`, `2026 Checklist`), or primary value props.
|
|
269
|
+
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."*).
|
|
271
|
+
6. **Heading Polish**: Ensure `<h1>` matches search intent and **never ends with a period (`.`)**.
|
|
272
|
+
|
|
273
|
+
#### Step 4: Immediately Trigger Googlebot Indexing
|
|
274
|
+
```bash
|
|
275
|
+
# Push the updated URL to Google Indexing API for rapid re-crawl within hours
|
|
276
|
+
gsc index https://example.com/optimized-page --json
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### 9. Google Trends & Universal Keyword Ingestion
|
|
282
|
+
When researching new topics or expanding keyword coverage:
|
|
283
|
+
```bash
|
|
284
|
+
# Real-time search demand curve, rising breakout queries, and seasonal interest
|
|
285
|
+
gsc trends "moving checklist" --json
|
|
286
|
+
|
|
287
|
+
# Universal import from Keywords Everywhere CSV/TSV or Google Keyword Planner export
|
|
288
|
+
gsc import path/to/keywords.csv --json
|
|
289
|
+
|
|
290
|
+
# Ingest copied table directly from clipboard (from 500 free daily web lookups)
|
|
291
|
+
gsc import clip --json
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### 10. AI SEO Playbook Catalog & Ready-to-Run Prompts (`gsc prompts`)
|
|
299
|
+
GSC CLI includes a curated registry of **25 battle-tested, high-impact AI SEO Playbooks** with dynamically rendered prompts and underlying CLI commands:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Query all 25 intelligent SEO playbooks in structured JSON
|
|
303
|
+
gsc prompts --json
|
|
304
|
+
|
|
305
|
+
# Query a specific playbook by ID
|
|
306
|
+
gsc prompt 5 --json
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### The 6 Strategic Playbook Categories:
|
|
310
|
+
1. **🚀 Growth & Striking Distance (Playbooks 1–4)**: Page 2 leaps (positions 8–18), untargeted keyword goldmines, Google Trends seasonal surges, and long-tail autocomplete multipliers.
|
|
311
|
+
2. **🎯 Conversion & CTR Multipliers (Playbooks 5–8)**: CTR underperformer doubles (<2% CTR on Page 1), Review/FAQ rich snippet enablers, <h1> headline alignment, and keyword cannibalization consolidation.
|
|
312
|
+
3. **📊 GA4 Behavioral & Ad Synergy (Playbooks 9–12)**: High-bounce traffic leak pluggers, real-time traffic wave riders, Google Ads vs Organic synergy (cut wasted ad spend), and omni-channel attribution audits.
|
|
313
|
+
4. **🛠️ Technical Health & Crawl Optimization (Playbooks 13–16)**: 90-day zombie page crawl purges, sitemap coverage & indexing blitzes, ranking decay early warning alerts, and lost query resuscitation.
|
|
314
|
+
5. **⚡ Programmatic SEO & Scaling (Playbooks 17–20)**: Programmatic landing page generators from saved keyword archives, competitor gap exploitation, zero-click search/AI Overview winning strategies, and mobile vs desktop SERP parity.
|
|
315
|
+
6. **📋 Executive Briefings & Daily Standups (Playbooks 21–25)**: 360° C-Suite monthly health briefings, new feature launch indexing blitzes, international market expansion diagnostics, and 5-minute daily SEO standups.
|
|
316
|
+
|
|
317
|
+
**Agent Playbook Execution Protocol**:
|
|
318
|
+
When the user asks open-ended questions like *"How can we grow traffic this week?"* or *"What SEO tasks should we work on?"*:
|
|
319
|
+
1. Run `gsc prompts --json` to load the playbook library.
|
|
320
|
+
2. Run `gsc performance --days 30 --json` and `gsc saved check 1 --json` to diagnose site opportunities.
|
|
321
|
+
3. Select the 2–3 highest-impact playbooks for the site's current state and present them clearly to the user, offering to execute them immediately.
|
|
322
|
+
|
|
323
|
+
## Error Handling & Onboarding
|
|
324
|
+
- If `gsc` returns an error about missing credentials: Direct the user to run `gsc connect` in their terminal (which automatically scans `~/Downloads` for service account JSON keys or accepts a drag-and-drop), or run `gsc open` to reveal `~/.config/gsc/` in Finder.
|
|
325
|
+
- If `gsc` returns an error about permission restricted in Search Console: Ensure the service account email is added as an **Owner** in Google Search Console Settings -> Users and permissions.
|
|
326
|
+
- If `gsc` returns a 403 error for Google Analytics: Open Google Analytics (Admin > Account Access Management or Property Access Management), click '+', paste the service account email, and assign the **Viewer** role.
|
|
327
|
+
- If `gsc ke` returns `402 Insufficient Credits`: Inform the user that the REST API requires purchased credits, but they can perform 500 free daily lookups on `keywordseverywhere.com/tools/bulk-keywords-data`, click **Copy**, and run `gsc import clip` to ingest the data with full opportunity scoring and sparklines for free!
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
SKILL
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
COMMAND_REGISTRY = CommandRegistry::COMMAND_REGISTRY
|
|
334
|
+
SKILL_MD_CONTENT = CommandRegistry::SKILL_MD_CONTENT
|
|
335
|
+
end
|
data/lib/gsc/config.rb
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module GSC
|
|
8
|
+
class Config
|
|
9
|
+
CONFIG_DIR = File.expand_path('~/.config/gsc')
|
|
10
|
+
CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
|
|
11
|
+
|
|
12
|
+
def self.load
|
|
13
|
+
return {} unless File.exist?(CONFIG_FILE)
|
|
14
|
+
JSON.parse(File.read(CONFIG_FILE))
|
|
15
|
+
rescue StandardError
|
|
16
|
+
{}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.save(data)
|
|
20
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
|
21
|
+
File.chmod(0700, CONFIG_DIR) rescue nil
|
|
22
|
+
existing = load
|
|
23
|
+
updated = existing.merge(data)
|
|
24
|
+
File.write(CONFIG_FILE, JSON.pretty_generate(updated))
|
|
25
|
+
File.chmod(0600, CONFIG_FILE) rescue nil
|
|
26
|
+
updated
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.default_domain
|
|
30
|
+
load['default_domain']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.set_default_domain(domain)
|
|
34
|
+
clean = domain.to_s.sub(%r{^https?://}, '').sub(/^sc-domain:/, '').chomp('/')
|
|
35
|
+
save('default_domain' => clean)
|
|
36
|
+
clean
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.key_path
|
|
40
|
+
load['key_path']
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.set_key_path(path)
|
|
44
|
+
resolved = File.expand_path(path)
|
|
45
|
+
save('key_path' => resolved)
|
|
46
|
+
resolved
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.ga4_properties
|
|
50
|
+
load['ga4_properties'] || {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.ga4_property_id(domain = nil)
|
|
54
|
+
dom = domain || default_domain
|
|
55
|
+
return load['ga4_property_id'] unless dom
|
|
56
|
+
clean = dom.to_s.sub(%r{^https?://}, '').sub(/^sc-domain:/, '').chomp('/')
|
|
57
|
+
ga4_properties[clean] || load['ga4_property_id']
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.set_ga4_property_id(property_id, domain = nil)
|
|
61
|
+
dom = domain || default_domain
|
|
62
|
+
clean = dom ? dom.to_s.sub(%r{^https?://}, '').sub(/^sc-domain:/, '').chomp('/') : nil
|
|
63
|
+
clean_id = property_id.to_s.strip.sub(%r{^properties/}, '')
|
|
64
|
+
|
|
65
|
+
if clean
|
|
66
|
+
props = ga4_properties.dup
|
|
67
|
+
props[clean] = clean_id
|
|
68
|
+
save('ga4_properties' => props)
|
|
69
|
+
else
|
|
70
|
+
save('ga4_property_id' => clean_id)
|
|
71
|
+
end
|
|
72
|
+
clean_id
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.unlink_ga4_property(domain = nil)
|
|
76
|
+
dom = domain || default_domain
|
|
77
|
+
return unless dom
|
|
78
|
+
clean = dom.to_s.sub(%r{^https?://}, '').sub(/^sc-domain:/, '').chomp('/')
|
|
79
|
+
props = ga4_properties.dup
|
|
80
|
+
removed = props.delete(clean)
|
|
81
|
+
save('ga4_properties' => props)
|
|
82
|
+
removed
|
|
83
|
+
end
|
|
84
|
+
def self.keywords_everywhere_api_key
|
|
85
|
+
ENV['KEYWORDSEVERYWHERE_API_KEY'] || load['keywords_everywhere_api_key'] || load['ke_api_key']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.set_keywords_everywhere_api_key(key)
|
|
89
|
+
clean = key.to_s.strip
|
|
90
|
+
save('keywords_everywhere_api_key' => clean)
|
|
91
|
+
clean
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.domain_keywords_dir(domain = nil)
|
|
95
|
+
dom = domain || default_domain || 'global'
|
|
96
|
+
clean = dom.to_s.sub(%r{^https?://}, '').sub(/^sc-domain:/, '').chomp('/').gsub(/[^a-zA-Z0-9.-]/, '_')
|
|
97
|
+
dir = File.join(CONFIG_DIR, 'domains', clean, 'keywords')
|
|
98
|
+
FileUtils.mkdir_p(dir)
|
|
99
|
+
File.chmod(0700, dir) rescue nil
|
|
100
|
+
dir
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.save_keyword_research(domain, name_or_seed, data, source: 'planner')
|
|
104
|
+
dir = domain_keywords_dir(domain)
|
|
105
|
+
date_str = Time.now.strftime('%Y-%m-%d')
|
|
106
|
+
clean_name = name_or_seed.to_s.downcase.gsub(/[^a-z0-9]+/, '-').sub(/^-+/, '').sub(/-+$/, '')
|
|
107
|
+
clean_name = 'research' if clean_name.empty?
|
|
108
|
+
filename = "#{date_str}-#{clean_name}.json"
|
|
109
|
+
filepath = File.join(dir, filename)
|
|
110
|
+
|
|
111
|
+
payload = {
|
|
112
|
+
'domain' => domain || default_domain,
|
|
113
|
+
'seed' => name_or_seed,
|
|
114
|
+
'source' => source,
|
|
115
|
+
'savedAt' => Time.now.utc.iso8601,
|
|
116
|
+
'totalKeywords' => data.size,
|
|
117
|
+
'keywords' => data
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
File.write(filepath, JSON.pretty_generate(payload))
|
|
121
|
+
filepath
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.list_saved_keywords(domain = nil)
|
|
125
|
+
dir = domain_keywords_dir(domain)
|
|
126
|
+
files = Dir.glob(File.join(dir, '*.json')).sort_by { |f| File.mtime(f) }.reverse
|
|
127
|
+
files.map do |f|
|
|
128
|
+
parsed = JSON.parse(File.read(f)) rescue {}
|
|
129
|
+
{
|
|
130
|
+
'file' => File.basename(f),
|
|
131
|
+
'path' => f,
|
|
132
|
+
'seed' => parsed['seed'] || File.basename(f, '.json'),
|
|
133
|
+
'source' => parsed['source'] || 'unknown',
|
|
134
|
+
'savedAt' => parsed['savedAt'] || File.mtime(f).utc.iso8601,
|
|
135
|
+
'totalKeywords' => parsed['totalKeywords'] || (parsed['keywords'] ? parsed['keywords'].size : 0),
|
|
136
|
+
'domain' => parsed['domain']
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.load_saved_keywords(domain, identifier)
|
|
142
|
+
list = list_saved_keywords(domain)
|
|
143
|
+
return nil if list.empty?
|
|
144
|
+
|
|
145
|
+
target_file = if identifier.to_s =~ /^\d+$/
|
|
146
|
+
idx = identifier.to_i - 1
|
|
147
|
+
list[idx]&.fetch('path', nil)
|
|
148
|
+
else
|
|
149
|
+
found = list.find { |item| item['file'] == identifier || item['seed'] == identifier || item['file'].include?(identifier.to_s) }
|
|
150
|
+
found ? found['path'] : nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return nil unless target_file && File.exist?(target_file)
|
|
154
|
+
JSON.parse(File.read(target_file))
|
|
155
|
+
rescue StandardError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def self.delete_saved_keywords(domain, identifier)
|
|
160
|
+
list = list_saved_keywords(domain)
|
|
161
|
+
return false if list.empty?
|
|
162
|
+
|
|
163
|
+
target_file = if identifier.to_s =~ /^\d+$/
|
|
164
|
+
idx = identifier.to_i - 1
|
|
165
|
+
list[idx]&.fetch('path', nil)
|
|
166
|
+
else
|
|
167
|
+
found = list.find { |item| item['file'] == identifier || item['file'].include?(identifier.to_s) }
|
|
168
|
+
found ? found['path'] : nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
return false unless target_file && File.exist?(target_file)
|
|
172
|
+
File.delete(target_file)
|
|
173
|
+
true
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|