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.
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
+