geo-ai-search-optimization 1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # geo-ai-search-optimization
2
+
3
+ Install and run a production-ready Codex skill for GEO-first, SEO-supported website optimization.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g geo-ai-search-optimization
9
+ ```
10
+
11
+ Or run it without a permanent install:
12
+
13
+ ```bash
14
+ npx geo-ai-search-optimization
15
+ ```
16
+
17
+ ## What it does
18
+
19
+ - installs the bundled `geo-ai-search-optimization` skill into your Codex skills directory
20
+ - ships a local resource folder with `SKILL.md`, references, and a scanner script
21
+ - provides a CLI for installing, locating, and scanning projects for GEO signals
22
+
23
+ ## Commands
24
+
25
+ ```bash
26
+ geo-ai-search-optimization
27
+ geo-ai-search-optimization install
28
+ geo-ai-search-optimization where
29
+ geo-ai-search-optimization scan ./my-site
30
+ geo-ai-search-optimization help
31
+ ```
32
+
33
+ ## Install location
34
+
35
+ By default the skill is installed to:
36
+
37
+ - macOS / Linux: `~/.codex/skills/geo-ai-search-optimization`
38
+ - Windows: `%USERPROFILE%\\.codex\\skills\\geo-ai-search-optimization`
39
+
40
+ Override with:
41
+
42
+ - `CODEX_HOME`
43
+ - `CODEX_SKILLS_DIR`
44
+ - `GEO_SKILL_INSTALL_DIR`
45
+
46
+ ## Resource contents
47
+
48
+ The installed skill includes:
49
+
50
+ - `SKILL.md`
51
+ - `agents/openai.yaml`
52
+ - `references/geo-playbook.md`
53
+ - `references/implementation-checklist.md`
54
+ - `scripts/scan-geo-signals.mjs`
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from "../src/cli.js";
4
+
5
+ try {
6
+ await runCli(process.argv.slice(2));
7
+ } catch (error) {
8
+ process.stderr.write(`geo-ai-search-optimization: ${error.message}\n`);
9
+ process.exitCode = 1;
10
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "geo-ai-search-optimization",
3
+ "version": "1.0.0",
4
+ "description": "Install and run a GEO-first, SEO-supported Codex skill for AI search optimization.",
5
+ "type": "module",
6
+ "bin": {
7
+ "geo-ai-search-optimization": "bin/geo-ai-search-optimization.js"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "resources",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "scripts": {
23
+ "postinstall": "node ./src/postinstall.js",
24
+ "check": "node ./src/cli.js --help",
25
+ "scan:self": "node ./src/cli.js scan ./resources/geo-ai-search-optimization"
26
+ },
27
+ "keywords": [
28
+ "geo",
29
+ "seo",
30
+ "ai-search",
31
+ "answer-engine-optimization",
32
+ "codex-skill",
33
+ "chatgpt",
34
+ "perplexity",
35
+ "gemini"
36
+ ],
37
+ "license": "MIT"
38
+ }
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: geo-ai-search-optimization
3
+ description: Optimize websites for GEO-first, SEO-supported visibility in AI-powered search and answer engines. Use when Codex needs to audit or improve a website, content system, landing page, blog, docs site, ecommerce catalog, or local business presence for ChatGPT, Claude, Perplexity, Gemini, Google AI Overviews, generative engine optimization, answer-engine optimization, AI search discoverability, structured data, citation readiness, or SEO work that must prioritize AI-driven discovery.
4
+ ---
5
+
6
+ # GEO AI Search Optimization
7
+
8
+ Optimize for where search traffic is moving: answer engines, AI overviews, and citation-heavy assistants. Preserve classic SEO foundations, but prioritize answerability, source quality, entity clarity, and crawlable evidence.
9
+
10
+ ## Core Positioning
11
+
12
+ Treat GEO as the primary objective and SEO as the support layer.
13
+
14
+ Use this skill to:
15
+
16
+ - audit an existing site or repo for AI-search readiness
17
+ - rewrite pages so they are easier for models to quote, summarize, and cite
18
+ - plan new content systems around questions, entities, proof, and comparisons
19
+ - implement technical supports such as schema, canonicals, robots, sitemaps, authorship, and citation surfaces
20
+
21
+ Do not optimize for keyword stuffing, thin FAQ spam, doorway pages, fake expertise, or generic AI-generated filler.
22
+
23
+ ## Workflow
24
+
25
+ ### 1. Classify the task
26
+
27
+ Choose one primary mode before making recommendations:
28
+
29
+ - `audit`: inspect an existing site, codebase, or exported HTML
30
+ - `rewrite`: improve one or more pages for answer-engine visibility
31
+ - `build`: create a GEO content plan, template system, or implementation brief
32
+ - `measure`: define observable signals, KPIs, and change tracking
33
+
34
+ If the user gives both content and code, start with `audit`, then branch into `rewrite` or `build`.
35
+
36
+ ### 2. Establish the evidence surface
37
+
38
+ Look for the assets that make answers trustworthy and quotable:
39
+
40
+ - named entities: brand, product, person, company, location, category
41
+ - explicit claims: pricing, specs, definitions, comparisons, policies, methodology
42
+ - first-party evidence: research, data, benchmarks, screenshots, customer examples, case studies
43
+ - source transparency: dates, authors, reviewers, citations, outbound references
44
+
45
+ If these are weak, say so clearly. AI-search performance often fails because there is nothing distinctive to cite.
46
+
47
+ ### 3. Check technical foundations
48
+
49
+ Confirm the site is easy to crawl, interpret, and attribute:
50
+
51
+ - indexable and canonical URLs
52
+ - sensible internal linking and stable information architecture
53
+ - `robots.txt`, sitemap support, and predictable URL patterns
54
+ - strong titles, descriptions, headings, and on-page summaries
55
+ - JSON-LD or equivalent structured data where it helps disambiguation
56
+ - visible authorship, freshness, and organization identity
57
+
58
+ For repo-based work, run the local scanner first:
59
+
60
+ ```bash
61
+ node scripts/scan-geo-signals.mjs /path/to/project
62
+ ```
63
+
64
+ Or, if the npm package is available:
65
+
66
+ ```bash
67
+ npx geo-ai-search-optimization scan /path/to/project
68
+ ```
69
+
70
+ Use the scanner output as a starting map, not as the final judgment.
71
+
72
+ ### 4. Improve answerability before chasing rankings
73
+
74
+ Restructure content so a model can extract a high-confidence answer quickly:
75
+
76
+ - put the direct answer near the top
77
+ - define the entity before expanding on it
78
+ - use specific headings that mirror real user questions
79
+ - add short summary blocks, comparison tables, pros and cons, and decision criteria
80
+ - include citations or clear sourcing for non-obvious claims
81
+ - separate facts, opinions, and marketing language
82
+
83
+ When useful, add FAQ, glossary, alternatives, comparison, methodology, statistics, or policy sections. Prefer dense, useful blocks over long intros.
84
+
85
+ ### 5. Strengthen citation readiness
86
+
87
+ Increase the chance that answer engines can quote or paraphrase accurately:
88
+
89
+ - attribute content to real experts, teams, or organizations
90
+ - expose original data, unique process details, or product-specific knowledge
91
+ - link to primary or authoritative sources when claims are not first-party
92
+ - add `sameAs`, organization, author, article, FAQ, product, local business, or breadcrumb schema when relevant
93
+ - make dates, update cadence, and version context explicit
94
+
95
+ If content sounds interchangeable with competitors, call that out and recommend a stronger point of view or original evidence.
96
+
97
+ ### 6. Produce prioritized output
98
+
99
+ Always return recommendations in this order:
100
+
101
+ 1. blockers: issues that prevent crawlability, attribution, or answer extraction
102
+ 2. high-impact GEO fixes: changes that improve answerability and citation probability
103
+ 3. SEO support fixes: enhancements that reinforce discoverability and consistency
104
+ 4. experiments: optional tests such as new page types, FAQ expansions, or comparison hubs
105
+
106
+ Prefer a short implementation backlog with owner-ready tasks over a long essay.
107
+
108
+ ## Common Deliverables
109
+
110
+ ### Audit deliverable
111
+
112
+ Return:
113
+
114
+ - current state summary
115
+ - strongest citation assets already present
116
+ - missing trust or structure signals
117
+ - prioritized fixes by effort and impact
118
+
119
+ Read [references/implementation-checklist.md](references/implementation-checklist.md) when you need a fuller checklist.
120
+
121
+ ### Rewrite deliverable
122
+
123
+ For page rewrites:
124
+
125
+ - preserve factual accuracy
126
+ - tighten the opening into a direct answer
127
+ - add definitions, evidence, and question-led subheads
128
+ - surface author, reviewer, date, and source notes when possible
129
+ - keep the page useful for humans first
130
+
131
+ Read [references/geo-playbook.md](references/geo-playbook.md) when you need content patterns.
132
+
133
+ ### Build deliverable
134
+
135
+ For new systems, propose:
136
+
137
+ - page templates by intent
138
+ - schema plan by page type
139
+ - evidence collection workflow
140
+ - editorial rules for freshness, citations, and entity consistency
141
+ - measurement plan tied to both AI-search and traditional SEO outcomes
142
+
143
+ ## Execution Notes
144
+
145
+ When the user asks for a codebase implementation:
146
+
147
+ - inspect templates, layouts, metadata helpers, routing, and schema generation points
148
+ - modify reusable primitives instead of patching one page at a time when possible
149
+ - add examples for the highest-value page type first
150
+
151
+ When the user asks for strategic recommendations:
152
+
153
+ - anchor every recommendation to a page type, entity, or evidence gap
154
+ - avoid broad advice like "publish better content" without specifying the format and missing proof
155
+
156
+ When the user asks for the latest vendor behavior or policy:
157
+
158
+ - verify with current primary sources before making claims about Google AI Overviews, ChatGPT, Gemini, Claude, Perplexity, or schema support
159
+
160
+ ## Bundled Resources
161
+
162
+ - `scripts/scan-geo-signals.mjs`: local scanner for crawlability, schema, answerability, and citation signals
163
+ - `references/geo-playbook.md`: content patterns and GEO-first page design heuristics
164
+ - `references/implementation-checklist.md`: technical, content, and measurement checklist
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "GEO for AI Search"
3
+ short_description: "Optimize sites for AI search and GEO workflows"
4
+ default_prompt: "Use $geo-ai-search-optimization to audit this site for AI-search discoverability and recommend GEO-first, SEO-supported fixes."
@@ -0,0 +1,130 @@
1
+ # GEO Playbook
2
+
3
+ ## Table of Contents
4
+
5
+ - Answer-engine content model
6
+ - Page patterns that tend to travel into AI answers
7
+ - Evidence patterns
8
+ - Rewrite heuristics
9
+ - Anti-patterns
10
+
11
+ ## Answer-engine content model
12
+
13
+ Design pages so a model can answer four questions fast:
14
+
15
+ 1. What is this entity?
16
+ 2. Why should this source be trusted?
17
+ 3. What is the direct answer?
18
+ 4. What evidence supports the answer?
19
+
20
+ Make those answers visible in the page structure, not buried in prose.
21
+
22
+ ## Page patterns that tend to travel into AI answers
23
+
24
+ ### Definition pages
25
+
26
+ Use for category, concept, feature, or industry terminology.
27
+
28
+ Include:
29
+
30
+ - one-sentence definition near the top
31
+ - who it is for
32
+ - how it differs from adjacent concepts
33
+ - short examples
34
+ - linked deeper pages for workflow, pricing, or implementation
35
+
36
+ ### Comparison pages
37
+
38
+ Use for `x vs y`, alternatives, replacement decisions, and buyer evaluation.
39
+
40
+ Include:
41
+
42
+ - explicit comparison criteria
43
+ - table format when possible
44
+ - ideal fit by segment or use case
45
+ - known tradeoffs
46
+ - update date if the market changes quickly
47
+
48
+ ### FAQ hubs
49
+
50
+ Use only when questions are real and distinct.
51
+
52
+ Include:
53
+
54
+ - one direct answer per question
55
+ - a short expansion paragraph
56
+ - links to the canonical page for deeper detail
57
+
58
+ Avoid manufacturing low-value questions just to increase count.
59
+
60
+ ### Methodology and research pages
61
+
62
+ Use when the brand has original data or proprietary process knowledge.
63
+
64
+ Include:
65
+
66
+ - what was measured
67
+ - date range
68
+ - sample size or scope
69
+ - method limitations
70
+ - key findings in summary form
71
+
72
+ These pages often provide the strongest citation material.
73
+
74
+ ### Local and service pages
75
+
76
+ Use for businesses with geographic presence or service areas.
77
+
78
+ Include:
79
+
80
+ - exact service entity and location
81
+ - operating area, hours, contact, and service constraints
82
+ - proof such as certifications, case studies, reviews, or before/after artifacts
83
+ - local business and organization identity signals
84
+
85
+ ## Evidence patterns
86
+
87
+ Prefer evidence in this order:
88
+
89
+ 1. first-party data or product truth
90
+ 2. primary sources
91
+ 3. authoritative secondary summaries
92
+ 4. internal opinion clearly labeled as opinion
93
+
94
+ Useful evidence blocks:
95
+
96
+ - benchmark tables
97
+ - methodology notes
98
+ - screenshots with captions
99
+ - pricing details
100
+ - implementation steps
101
+ - quote blocks from identifiable experts
102
+ - dated release or policy notes
103
+
104
+ ## Rewrite heuristics
105
+
106
+ When rewriting a page, apply this sequence:
107
+
108
+ 1. Replace vague intros with a direct answer.
109
+ 2. Name the entity precisely.
110
+ 3. Add one distinctive fact, method, or constraint early.
111
+ 4. Break out question-led subheads.
112
+ 5. Add comparison, checklist, or table sections where helpful.
113
+ 6. Add explicit sourcing for non-obvious claims.
114
+ 7. End with next-step links to deeper pages.
115
+
116
+ Good opening pattern:
117
+
118
+ `[Entity] is [short definition]. It is best for [audience/use case]. The main tradeoff is [constraint].`
119
+
120
+ ## Anti-patterns
121
+
122
+ Avoid:
123
+
124
+ - keyword-stuffed headings
125
+ - faceless articles with no author or owner
126
+ - listicles with no unique evidence
127
+ - copy that hides the answer behind long brand storytelling
128
+ - duplicated pages targeting tiny wording variations
129
+ - unsupported superlatives such as "best" or "most accurate"
130
+ - fake FAQ schema for questions not visible on the page
@@ -0,0 +1,75 @@
1
+ # GEO Implementation Checklist
2
+
3
+ ## Table of Contents
4
+
5
+ - Technical foundations
6
+ - Content and entity layer
7
+ - Structured data and attribution
8
+ - Measurement
9
+
10
+ ## Technical foundations
11
+
12
+ Check:
13
+
14
+ - canonical URLs exist and point to the preferred version
15
+ - robots rules do not block important pages
16
+ - sitemap or sitemap generation exists
17
+ - page titles and meta descriptions are defined at scale
18
+ - heading hierarchy is readable and specific
19
+ - internal links connect summaries to deeper canonical pages
20
+ - duplicate or near-duplicate routes are controlled
21
+ - important pages are server-rendered or otherwise accessible to crawlers
22
+
23
+ ## Content and entity layer
24
+
25
+ Check:
26
+
27
+ - each target page has one primary entity or decision topic
28
+ - the direct answer appears in the first screenful
29
+ - claims are tied to facts, examples, or sources
30
+ - authorship or responsible organization is visible
31
+ - freshness is visible through dates, version notes, or review notes
32
+ - comparison, FAQ, glossary, or methodology sections exist when intent requires them
33
+ - the page contains at least one piece of first-party value competitors cannot easily copy
34
+
35
+ ## Structured data and attribution
36
+
37
+ Use only the schema that matches the visible page content.
38
+
39
+ Common candidates:
40
+
41
+ - `Organization`
42
+ - `WebSite`
43
+ - `Person`
44
+ - `Article`
45
+ - `FAQPage`
46
+ - `HowTo`
47
+ - `Product`
48
+ - `BreadcrumbList`
49
+ - `LocalBusiness`
50
+
51
+ Also check:
52
+
53
+ - `sameAs` links for organization or person entities when appropriate
54
+ - author or publisher details on article-like content
55
+ - product attributes that reflect the actual offer
56
+ - breadcrumb markup aligned with site structure
57
+
58
+ ## Measurement
59
+
60
+ Track both AI-search and classic SEO outcomes.
61
+
62
+ Suggested measures:
63
+
64
+ - impressions and clicks from traditional search
65
+ - branded and non-branded query coverage
66
+ - referral traffic from AI/search assistants when observable
67
+ - growth in pages that earn citations, mentions, or backlinks
68
+ - engagement on comparison, FAQ, glossary, and methodology pages
69
+ - faster content production for answer-focused templates
70
+
71
+ For implementation backlogs, tag each item with:
72
+
73
+ - impact: `blocker`, `high`, `medium`, `low`
74
+ - type: `technical`, `content`, `schema`, `measurement`
75
+ - owner: engineering, content, SEO, product marketing, or subject-matter expert
@@ -0,0 +1,254 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const TEXT_EXTENSIONS = new Set([
5
+ ".astro",
6
+ ".html",
7
+ ".htm",
8
+ ".js",
9
+ ".jsx",
10
+ ".liquid",
11
+ ".md",
12
+ ".mdx",
13
+ ".mjs",
14
+ ".njk",
15
+ ".php",
16
+ ".svelte",
17
+ ".ts",
18
+ ".tsx",
19
+ ".txt",
20
+ ".vue"
21
+ ]);
22
+
23
+ const IGNORE_DIRS = new Set([
24
+ ".git",
25
+ ".hg",
26
+ ".next",
27
+ ".nuxt",
28
+ ".output",
29
+ ".svelte-kit",
30
+ "build",
31
+ "coverage",
32
+ "dist",
33
+ "node_modules",
34
+ "out",
35
+ "tmp",
36
+ "vendor"
37
+ ]);
38
+
39
+ const SIGNAL_PATTERNS = {
40
+ title: /<title\b|metadata\s*[:=]\s*\{|title\s*:\s*['"]/i,
41
+ meta_description: /name=["']description["']|description\s*:\s*["']/i,
42
+ canonical: /rel=["']canonical["']|canonical\s*:\s*["']|alternates\s*:\s*\{/i,
43
+ json_ld: /application\/ld\+json|schema\.org/i,
44
+ faq_schema: /@type["']?\s*[:=]\s*["']FAQPage["']|schema\.org\/FAQPage/i,
45
+ howto_schema: /@type["']?\s*[:=]\s*["']HowTo["']|schema\.org\/HowTo/i,
46
+ article_schema:
47
+ /@type["']?\s*[:=]\s*["'](Article|BlogPosting|NewsArticle)["']|schema\.org\/(Article|BlogPosting|NewsArticle)/i,
48
+ organization_schema:
49
+ /@type["']?\s*[:=]\s*["'](Organization|Corporation|LocalBusiness)["']|schema\.org\/(Organization|Corporation|LocalBusiness)/i,
50
+ product_schema:
51
+ /@type["']?\s*[:=]\s*["'](Product|SoftwareApplication)["']|schema\.org\/(Product|SoftwareApplication)/i,
52
+ breadcrumb_schema:
53
+ /@type["']?\s*[:=]\s*["']BreadcrumbList["']|schema\.org\/BreadcrumbList/i,
54
+ author_markers: /\bauthor\b|\breviewed by\b|\bpublisher\b|\bdatePublished\b|\bdateModified\b/i,
55
+ qa_headings: /^(#{1,6}\s+.+\?)|(<h[1-6][^>]*>.*\?.*<\/h[1-6]>)/im,
56
+ comparison_intent: /\b(vs\.?|versus|alternative to|alternatives to|compare)\b/i,
57
+ original_research: /\b(benchmark|survey|study|dataset|methodology|research|report)\b/i,
58
+ citations: /https?:\/\//i
59
+ };
60
+
61
+ async function collectFiles(root) {
62
+ const files = [];
63
+ const queue = [root];
64
+
65
+ while (queue.length > 0) {
66
+ const current = queue.shift();
67
+ const entries = await fs.readdir(current, { withFileTypes: true });
68
+
69
+ for (const entry of entries) {
70
+ const entryPath = path.join(current, entry.name);
71
+ if (entry.isDirectory()) {
72
+ if (!IGNORE_DIRS.has(entry.name)) {
73
+ queue.push(entryPath);
74
+ }
75
+ continue;
76
+ }
77
+
78
+ const extension = path.extname(entry.name).toLowerCase();
79
+ if (TEXT_EXTENSIONS.has(extension) || entry.name === "robots.txt" || entry.name === "llms.txt") {
80
+ files.push(entryPath);
81
+ }
82
+ }
83
+ }
84
+
85
+ return files;
86
+ }
87
+
88
+ async function readText(filePath, maxFileSize) {
89
+ try {
90
+ const stat = await fs.stat(filePath);
91
+ if (stat.size > maxFileSize) {
92
+ return "";
93
+ }
94
+ return await fs.readFile(filePath, "utf8");
95
+ } catch {
96
+ return "";
97
+ }
98
+ }
99
+
100
+ function buildRecommendations(summary) {
101
+ const recommendations = [];
102
+ const { signals, specialFiles } = summary;
103
+
104
+ if (specialFiles.robotsTxt.length === 0) {
105
+ recommendations.push("Add or verify robots.txt so crawlers and answer engines see clear crawl rules.");
106
+ }
107
+ if (specialFiles.sitemap.length === 0) {
108
+ recommendations.push("Add a sitemap or sitemap generator to expose canonical URLs at scale.");
109
+ }
110
+ if (signals.canonical.count === 0) {
111
+ recommendations.push("Implement canonical tags or metadata helpers to reduce duplicate URL ambiguity.");
112
+ }
113
+ if (signals.json_ld.count === 0) {
114
+ recommendations.push("Add visible, page-matching structured data to improve entity disambiguation and citation confidence.");
115
+ }
116
+ if (signals.author_markers.count === 0) {
117
+ recommendations.push("Expose authorship, reviewer, publisher, or update metadata on advice pages.");
118
+ }
119
+ if (signals.qa_headings.count === 0) {
120
+ recommendations.push("Introduce question-led headings or FAQ sections on pages with informational intent.");
121
+ }
122
+ if (signals.citations.count === 0) {
123
+ recommendations.push("Add source links for non-obvious claims, benchmarks, or market statements.");
124
+ }
125
+ if (signals.original_research.count === 0) {
126
+ recommendations.push("Create at least one methodology, benchmark, or first-party evidence page that competitors cannot easily copy.");
127
+ }
128
+ if (signals.comparison_intent.count === 0) {
129
+ recommendations.push("Consider comparison or alternatives pages if you compete in a crowded category.");
130
+ }
131
+
132
+ return recommendations;
133
+ }
134
+
135
+ async function scanProject(rootInput, options = {}) {
136
+ const root = path.resolve(rootInput);
137
+ const maxFileSize = options.maxFileSize || 1_000_000;
138
+ const maxExamples = options.maxExamples || 5;
139
+
140
+ const stat = await fs.stat(root).catch(() => null);
141
+ if (!stat) {
142
+ throw new Error(`Path not found: ${root}`);
143
+ }
144
+ if (!stat.isDirectory()) {
145
+ throw new Error(`Path is not a directory: ${root}`);
146
+ }
147
+
148
+ const files = await collectFiles(root);
149
+ const counts = Object.fromEntries(Object.keys(SIGNAL_PATTERNS).map((key) => [key, 0]));
150
+ const examples = Object.fromEntries(Object.keys(SIGNAL_PATTERNS).map((key) => [key, []]));
151
+ const specialFiles = {
152
+ robotsTxt: [],
153
+ sitemap: [],
154
+ llmsTxt: []
155
+ };
156
+
157
+ let filesScanned = 0;
158
+
159
+ for (const filePath of files) {
160
+ const relativePath = path.relative(root, filePath);
161
+ const baseName = path.basename(filePath).toLowerCase();
162
+
163
+ if (baseName === "robots.txt") {
164
+ specialFiles.robotsTxt.push(relativePath);
165
+ }
166
+ if (baseName === "llms.txt") {
167
+ specialFiles.llmsTxt.push(relativePath);
168
+ }
169
+ if (baseName.includes("sitemap")) {
170
+ specialFiles.sitemap.push(relativePath);
171
+ }
172
+
173
+ const content = await readText(filePath, maxFileSize);
174
+ if (!content) {
175
+ continue;
176
+ }
177
+
178
+ filesScanned += 1;
179
+
180
+ for (const [signal, pattern] of Object.entries(SIGNAL_PATTERNS)) {
181
+ if (pattern.test(content)) {
182
+ counts[signal] += 1;
183
+ if (examples[signal].length < maxExamples) {
184
+ examples[signal].push(relativePath);
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ const summary = {
191
+ root,
192
+ filesConsidered: files.length,
193
+ filesScanned,
194
+ specialFiles,
195
+ signals: Object.fromEntries(
196
+ Object.keys(SIGNAL_PATTERNS).map((signal) => [
197
+ signal,
198
+ {
199
+ count: counts[signal],
200
+ examples: examples[signal]
201
+ }
202
+ ])
203
+ )
204
+ };
205
+
206
+ summary.recommendations = buildRecommendations(summary);
207
+ return summary;
208
+ }
209
+
210
+ function renderScanMarkdown(summary) {
211
+ const lines = [
212
+ "# GEO Signal Scan",
213
+ "",
214
+ `- Root: \`${summary.root}\``,
215
+ `- Files considered: \`${summary.filesConsidered}\``,
216
+ `- Files scanned as text: \`${summary.filesScanned}\``,
217
+ "",
218
+ "## Foundations",
219
+ "",
220
+ `- robots.txt: \`${summary.specialFiles.robotsTxt.join(", ") || "missing"}\``,
221
+ `- sitemap: \`${summary.specialFiles.sitemap.join(", ") || "missing"}\``,
222
+ `- llms.txt: \`${summary.specialFiles.llmsTxt.join(", ") || "not found"}\``,
223
+ "",
224
+ "## Signal Counts",
225
+ ""
226
+ ];
227
+
228
+ for (const [signal, details] of Object.entries(summary.signals)) {
229
+ const exampleText = details.examples.join(", ") || "none";
230
+ lines.push(`- ${signal}: \`${details.count}\` example(s): \`${exampleText}\``);
231
+ }
232
+
233
+ lines.push("", "## Recommended Next Moves", "");
234
+
235
+ if (summary.recommendations.length === 0) {
236
+ lines.push("- No obvious blockers detected by heuristics. Review page quality and evidence depth next.");
237
+ } else {
238
+ for (const recommendation of summary.recommendations) {
239
+ lines.push(`- ${recommendation}`);
240
+ }
241
+ }
242
+
243
+ return `${lines.join("\n")}\n`;
244
+ }
245
+
246
+ const [target = ".", ...flags] = process.argv.slice(2);
247
+ const outputJson = flags.includes("--json");
248
+ const summary = await scanProject(target);
249
+
250
+ if (outputJson) {
251
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
252
+ } else {
253
+ process.stdout.write(renderScanMarkdown(summary));
254
+ }
package/src/cli.js ADDED
@@ -0,0 +1,101 @@
1
+ import { installSkill } from "./install-skill.js";
2
+ import { getBundledSkillPath, getInstalledSkillPath, getSkillName, getSkillsDir } from "./paths.js";
3
+ import { renderScanMarkdown, scanProject } from "./scan.js";
4
+ import { fileURLToPath } from "node:url";
5
+ import { readFile } from "node:fs/promises";
6
+ import path from "node:path";
7
+
8
+ let cachedVersion;
9
+
10
+ async function getPackageVersion() {
11
+ if (cachedVersion) {
12
+ return cachedVersion;
13
+ }
14
+
15
+ const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
16
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
17
+ cachedVersion = packageJson.version;
18
+ return cachedVersion;
19
+ }
20
+
21
+ function printHelp() {
22
+ process.stdout.write(
23
+ [
24
+ "geo-ai-search-optimization",
25
+ "",
26
+ "Usage:",
27
+ " geo-ai-search-optimization",
28
+ " geo-ai-search-optimization install",
29
+ " geo-ai-search-optimization where",
30
+ " geo-ai-search-optimization scan <project-path> [--json]",
31
+ " geo-ai-search-optimization version",
32
+ " geo-ai-search-optimization help",
33
+ "",
34
+ "Environment overrides:",
35
+ " CODEX_HOME",
36
+ " CODEX_SKILLS_DIR",
37
+ " GEO_SKILL_INSTALL_DIR",
38
+ " GEO_SKILL_SKIP_POSTINSTALL"
39
+ ].join("\n") + "\n"
40
+ );
41
+ }
42
+
43
+ export async function runCli(args = []) {
44
+ const [command = "install", ...rest] = args;
45
+
46
+ if (command === "help" || command === "--help" || command === "-h") {
47
+ printHelp();
48
+ return;
49
+ }
50
+
51
+ if (command === "version" || command === "--version" || command === "-v") {
52
+ process.stdout.write(`${await getPackageVersion()}\n`);
53
+ return;
54
+ }
55
+
56
+ if (command === "install") {
57
+ await installSkill();
58
+ return;
59
+ }
60
+
61
+ if (command === "where") {
62
+ process.stdout.write(
63
+ [
64
+ `skill: ${getSkillName()}`,
65
+ `bundled: ${getBundledSkillPath()}`,
66
+ `skillsDir: ${getSkillsDir()}`,
67
+ `installed: ${getInstalledSkillPath()}`
68
+ ].join("\n") + "\n"
69
+ );
70
+ return;
71
+ }
72
+
73
+ if (command === "scan") {
74
+ const projectPath = rest.find((value) => !value.startsWith("-"));
75
+ if (!projectPath) {
76
+ throw new Error("scan requires a project path");
77
+ }
78
+
79
+ const outputJson = rest.includes("--json");
80
+ const summary = await scanProject(projectPath);
81
+ if (outputJson) {
82
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
83
+ } else {
84
+ process.stdout.write(renderScanMarkdown(summary));
85
+ }
86
+ return;
87
+ }
88
+
89
+ throw new Error(`Unknown command: ${command}`);
90
+ }
91
+
92
+ const isDirectRun = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
93
+
94
+ if (isDirectRun) {
95
+ try {
96
+ await runCli(process.argv.slice(2));
97
+ } catch (error) {
98
+ process.stderr.write(`geo-ai-search-optimization: ${error.message}\n`);
99
+ process.exitCode = 1;
100
+ }
101
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { installSkill } from "./install-skill.js";
2
+ export { runCli } from "./cli.js";
3
+ export { scanProject, renderScanMarkdown } from "./scan.js";
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getBundledSkillPath, getInstalledSkillPath, getSkillName } from "./paths.js";
4
+
5
+ async function pathExists(targetPath) {
6
+ try {
7
+ await fs.access(targetPath);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ export async function installSkill(options = {}) {
15
+ const sourceDir = getBundledSkillPath();
16
+ const targetDir = options.targetDir || getInstalledSkillPath();
17
+ const silent = Boolean(options.silent);
18
+
19
+ if (!(await pathExists(sourceDir))) {
20
+ throw new Error(`Bundled skill folder not found: ${sourceDir}`);
21
+ }
22
+
23
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
24
+ await fs.rm(targetDir, { recursive: true, force: true });
25
+ await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
26
+
27
+ const manifest = {
28
+ skill: getSkillName(),
29
+ installedAt: new Date().toISOString(),
30
+ installedFrom: sourceDir,
31
+ packageName: "geo-ai-search-optimization"
32
+ };
33
+
34
+ await fs.writeFile(
35
+ path.join(targetDir, ".installed-by-npm.json"),
36
+ `${JSON.stringify(manifest, null, 2)}\n`,
37
+ "utf8"
38
+ );
39
+
40
+ if (!silent) {
41
+ process.stdout.write(`Installed ${getSkillName()} to ${targetDir}\n`);
42
+ }
43
+
44
+ return {
45
+ targetDir,
46
+ sourceDir,
47
+ skillName: getSkillName()
48
+ };
49
+ }
package/src/paths.js ADDED
@@ -0,0 +1,33 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const RESOURCE_SKILL_NAME = "geo-ai-search-optimization";
6
+
7
+ export function getPackageRoot() {
8
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ }
10
+
11
+ export function getBundledSkillPath() {
12
+ return path.join(getPackageRoot(), "resources", RESOURCE_SKILL_NAME);
13
+ }
14
+
15
+ export function getCodexHome() {
16
+ return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
17
+ }
18
+
19
+ export function getSkillsDir() {
20
+ return (
21
+ process.env.GEO_SKILL_INSTALL_DIR ||
22
+ process.env.CODEX_SKILLS_DIR ||
23
+ path.join(getCodexHome(), "skills")
24
+ );
25
+ }
26
+
27
+ export function getInstalledSkillPath() {
28
+ return path.join(getSkillsDir(), RESOURCE_SKILL_NAME);
29
+ }
30
+
31
+ export function getSkillName() {
32
+ return RESOURCE_SKILL_NAME;
33
+ }
@@ -0,0 +1,12 @@
1
+ import { installSkill } from "./install-skill.js";
2
+
3
+ if (process.env.GEO_SKILL_SKIP_POSTINSTALL === "1") {
4
+ process.exit(0);
5
+ }
6
+
7
+ try {
8
+ await installSkill({ silent: true });
9
+ process.stdout.write("geo-ai-search-optimization: bundled skill installed\n");
10
+ } catch (error) {
11
+ process.stderr.write(`geo-ai-search-optimization: postinstall skipped (${error.message})\n`);
12
+ }
package/src/scan.js ADDED
@@ -0,0 +1,244 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const TEXT_EXTENSIONS = new Set([
5
+ ".astro",
6
+ ".html",
7
+ ".htm",
8
+ ".js",
9
+ ".jsx",
10
+ ".liquid",
11
+ ".md",
12
+ ".mdx",
13
+ ".mjs",
14
+ ".njk",
15
+ ".php",
16
+ ".svelte",
17
+ ".ts",
18
+ ".tsx",
19
+ ".txt",
20
+ ".vue"
21
+ ]);
22
+
23
+ const IGNORE_DIRS = new Set([
24
+ ".git",
25
+ ".hg",
26
+ ".next",
27
+ ".nuxt",
28
+ ".output",
29
+ ".svelte-kit",
30
+ "build",
31
+ "coverage",
32
+ "dist",
33
+ "node_modules",
34
+ "out",
35
+ "tmp",
36
+ "vendor"
37
+ ]);
38
+
39
+ const SIGNAL_PATTERNS = {
40
+ title: /<title\b|metadata\s*[:=]\s*\{|title\s*:\s*['"]/i,
41
+ meta_description: /name=["']description["']|description\s*:\s*["']/i,
42
+ canonical: /rel=["']canonical["']|canonical\s*:\s*["']|alternates\s*:\s*\{/i,
43
+ json_ld: /application\/ld\+json|schema\.org/i,
44
+ faq_schema: /@type["']?\s*[:=]\s*["']FAQPage["']|schema\.org\/FAQPage/i,
45
+ howto_schema: /@type["']?\s*[:=]\s*["']HowTo["']|schema\.org\/HowTo/i,
46
+ article_schema:
47
+ /@type["']?\s*[:=]\s*["'](Article|BlogPosting|NewsArticle)["']|schema\.org\/(Article|BlogPosting|NewsArticle)/i,
48
+ organization_schema:
49
+ /@type["']?\s*[:=]\s*["'](Organization|Corporation|LocalBusiness)["']|schema\.org\/(Organization|Corporation|LocalBusiness)/i,
50
+ product_schema:
51
+ /@type["']?\s*[:=]\s*["'](Product|SoftwareApplication)["']|schema\.org\/(Product|SoftwareApplication)/i,
52
+ breadcrumb_schema:
53
+ /@type["']?\s*[:=]\s*["']BreadcrumbList["']|schema\.org\/BreadcrumbList/i,
54
+ author_markers: /\bauthor\b|\breviewed by\b|\bpublisher\b|\bdatePublished\b|\bdateModified\b/i,
55
+ qa_headings: /^(#{1,6}\s+.+\?)|(<h[1-6][^>]*>.*\?.*<\/h[1-6]>)/im,
56
+ comparison_intent: /\b(vs\.?|versus|alternative to|alternatives to|compare)\b/i,
57
+ original_research: /\b(benchmark|survey|study|dataset|methodology|research|report)\b/i,
58
+ citations: /https?:\/\//i
59
+ };
60
+
61
+ async function collectFiles(root) {
62
+ const files = [];
63
+ const queue = [root];
64
+
65
+ while (queue.length > 0) {
66
+ const current = queue.shift();
67
+ const entries = await fs.readdir(current, { withFileTypes: true });
68
+
69
+ for (const entry of entries) {
70
+ const entryPath = path.join(current, entry.name);
71
+ if (entry.isDirectory()) {
72
+ if (!IGNORE_DIRS.has(entry.name)) {
73
+ queue.push(entryPath);
74
+ }
75
+ continue;
76
+ }
77
+
78
+ const extension = path.extname(entry.name).toLowerCase();
79
+ if (TEXT_EXTENSIONS.has(extension) || entry.name === "robots.txt" || entry.name === "llms.txt") {
80
+ files.push(entryPath);
81
+ }
82
+ }
83
+ }
84
+
85
+ return files;
86
+ }
87
+
88
+ async function readText(filePath, maxFileSize) {
89
+ try {
90
+ const stat = await fs.stat(filePath);
91
+ if (stat.size > maxFileSize) {
92
+ return "";
93
+ }
94
+ return await fs.readFile(filePath, "utf8");
95
+ } catch {
96
+ return "";
97
+ }
98
+ }
99
+
100
+ function buildRecommendations(summary) {
101
+ const recommendations = [];
102
+ const { signals, specialFiles } = summary;
103
+
104
+ if (specialFiles.robotsTxt.length === 0) {
105
+ recommendations.push("Add or verify robots.txt so crawlers and answer engines see clear crawl rules.");
106
+ }
107
+ if (specialFiles.sitemap.length === 0) {
108
+ recommendations.push("Add a sitemap or sitemap generator to expose canonical URLs at scale.");
109
+ }
110
+ if (signals.canonical.count === 0) {
111
+ recommendations.push("Implement canonical tags or metadata helpers to reduce duplicate URL ambiguity.");
112
+ }
113
+ if (signals.json_ld.count === 0) {
114
+ recommendations.push("Add visible, page-matching structured data to improve entity disambiguation and citation confidence.");
115
+ }
116
+ if (signals.author_markers.count === 0) {
117
+ recommendations.push("Expose authorship, reviewer, publisher, or update metadata on advice pages.");
118
+ }
119
+ if (signals.qa_headings.count === 0) {
120
+ recommendations.push("Introduce question-led headings or FAQ sections on pages with informational intent.");
121
+ }
122
+ if (signals.citations.count === 0) {
123
+ recommendations.push("Add source links for non-obvious claims, benchmarks, or market statements.");
124
+ }
125
+ if (signals.original_research.count === 0) {
126
+ recommendations.push("Create at least one methodology, benchmark, or first-party evidence page that competitors cannot easily copy.");
127
+ }
128
+ if (signals.comparison_intent.count === 0) {
129
+ recommendations.push("Consider comparison or alternatives pages if you compete in a crowded category.");
130
+ }
131
+
132
+ return recommendations;
133
+ }
134
+
135
+ export async function scanProject(rootInput, options = {}) {
136
+ const root = path.resolve(rootInput);
137
+ const maxFileSize = options.maxFileSize || 1_000_000;
138
+ const maxExamples = options.maxExamples || 5;
139
+
140
+ const stat = await fs.stat(root).catch(() => null);
141
+ if (!stat) {
142
+ throw new Error(`Path not found: ${root}`);
143
+ }
144
+ if (!stat.isDirectory()) {
145
+ throw new Error(`Path is not a directory: ${root}`);
146
+ }
147
+
148
+ const files = await collectFiles(root);
149
+ const counts = Object.fromEntries(Object.keys(SIGNAL_PATTERNS).map((key) => [key, 0]));
150
+ const examples = Object.fromEntries(Object.keys(SIGNAL_PATTERNS).map((key) => [key, []]));
151
+ const specialFiles = {
152
+ robotsTxt: [],
153
+ sitemap: [],
154
+ llmsTxt: []
155
+ };
156
+
157
+ let filesScanned = 0;
158
+
159
+ for (const filePath of files) {
160
+ const relativePath = path.relative(root, filePath);
161
+ const baseName = path.basename(filePath).toLowerCase();
162
+
163
+ if (baseName === "robots.txt") {
164
+ specialFiles.robotsTxt.push(relativePath);
165
+ }
166
+ if (baseName === "llms.txt") {
167
+ specialFiles.llmsTxt.push(relativePath);
168
+ }
169
+ if (baseName.includes("sitemap")) {
170
+ specialFiles.sitemap.push(relativePath);
171
+ }
172
+
173
+ const content = await readText(filePath, maxFileSize);
174
+ if (!content) {
175
+ continue;
176
+ }
177
+
178
+ filesScanned += 1;
179
+
180
+ for (const [signal, pattern] of Object.entries(SIGNAL_PATTERNS)) {
181
+ if (pattern.test(content)) {
182
+ counts[signal] += 1;
183
+ if (examples[signal].length < maxExamples) {
184
+ examples[signal].push(relativePath);
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ const summary = {
191
+ root,
192
+ filesConsidered: files.length,
193
+ filesScanned,
194
+ specialFiles,
195
+ signals: Object.fromEntries(
196
+ Object.keys(SIGNAL_PATTERNS).map((signal) => [
197
+ signal,
198
+ {
199
+ count: counts[signal],
200
+ examples: examples[signal]
201
+ }
202
+ ])
203
+ )
204
+ };
205
+
206
+ summary.recommendations = buildRecommendations(summary);
207
+ return summary;
208
+ }
209
+
210
+ export function renderScanMarkdown(summary) {
211
+ const lines = [
212
+ "# GEO Signal Scan",
213
+ "",
214
+ `- Root: \`${summary.root}\``,
215
+ `- Files considered: \`${summary.filesConsidered}\``,
216
+ `- Files scanned as text: \`${summary.filesScanned}\``,
217
+ "",
218
+ "## Foundations",
219
+ "",
220
+ `- robots.txt: \`${summary.specialFiles.robotsTxt.join(", ") || "missing"}\``,
221
+ `- sitemap: \`${summary.specialFiles.sitemap.join(", ") || "missing"}\``,
222
+ `- llms.txt: \`${summary.specialFiles.llmsTxt.join(", ") || "not found"}\``,
223
+ "",
224
+ "## Signal Counts",
225
+ ""
226
+ ];
227
+
228
+ for (const [signal, details] of Object.entries(summary.signals)) {
229
+ const exampleText = details.examples.join(", ") || "none";
230
+ lines.push(`- ${signal}: \`${details.count}\` example(s): \`${exampleText}\``);
231
+ }
232
+
233
+ lines.push("", "## Recommended Next Moves", "");
234
+
235
+ if (summary.recommendations.length === 0) {
236
+ lines.push("- No obvious blockers detected by heuristics. Review page quality and evidence depth next.");
237
+ } else {
238
+ for (const recommendation of summary.recommendations) {
239
+ lines.push(`- ${recommendation}`);
240
+ }
241
+ }
242
+
243
+ return `${lines.join("\n")}\n`;
244
+ }