geo-ai-search-optimization 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,11 +25,28 @@ npx geo-ai-search-optimization
25
25
  ```bash
26
26
  geo-ai-search-optimization
27
27
  geo-ai-search-optimization install
28
+ geo-ai-search-optimization install --target ./tmp/custom-skills --json
28
29
  geo-ai-search-optimization where
29
- geo-ai-search-optimization scan ./my-site
30
+ geo-ai-search-optimization doctor
31
+ geo-ai-search-optimization init-llms ./site --site-name "Acme Docs" --site-url "https://example.com"
32
+ geo-ai-search-optimization scan ./my-site --max-file-size 500000 --max-examples 3
33
+ geo-ai-search-optimization scan ./my-site --json --out ./reports/geo-scan.json
34
+ geo-ai-search-optimization version
30
35
  geo-ai-search-optimization help
31
36
  ```
32
37
 
38
+ ## New in 1.0.2
39
+
40
+ - `init-llms` command for generating an `llms.txt` starter template
41
+ - keeps the `1.0.1` CLI upgrades: `doctor`, custom `install --target`, and richer `scan` output controls
42
+
43
+ ## New in 1.0.1
44
+
45
+ - `doctor` command for installation and environment checks
46
+ - `install --target <dir>` for custom skill destinations
47
+ - `scan --out <file>` to save reports
48
+ - `scan --max-file-size` and `scan --max-examples` for tighter project scans
49
+
33
50
  ## Install location
34
51
 
35
52
  By default the skill is installed to:
@@ -42,6 +59,7 @@ Override with:
42
59
  - `CODEX_HOME`
43
60
  - `CODEX_SKILLS_DIR`
44
61
  - `GEO_SKILL_INSTALL_DIR`
62
+ - `GEO_SKILL_SKIP_POSTINSTALL`
45
63
 
46
64
  ## Resource contents
47
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geo-ai-search-optimization",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Install and run a GEO-first, SEO-supported Codex skill for AI search optimization.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,9 +1,11 @@
1
- import { installSkill } from "./install-skill.js";
2
- import { getBundledSkillPath, getInstalledSkillPath, getSkillName, getSkillsDir } from "./paths.js";
3
- import { renderScanMarkdown, scanProject } from "./scan.js";
4
1
  import { fileURLToPath } from "node:url";
5
2
  import { readFile } from "node:fs/promises";
6
3
  import path from "node:path";
4
+ import { installSkill } from "./install-skill.js";
5
+ import { getBundledSkillPath, getInstalledSkillPath, getSkillName, getSkillsDir } from "./paths.js";
6
+ import { renderScanMarkdown, scanProject, writeScanOutput } from "./scan.js";
7
+ import { renderDoctorMarkdown, runDoctor } from "./doctor.js";
8
+ import { createLlmsTxt } from "./llms-txt.js";
7
9
 
8
10
  let cachedVersion;
9
11
 
@@ -25,9 +27,11 @@ function printHelp() {
25
27
  "",
26
28
  "Usage:",
27
29
  " geo-ai-search-optimization",
28
- " geo-ai-search-optimization install",
30
+ " geo-ai-search-optimization install [--target <dir>] [--json]",
29
31
  " geo-ai-search-optimization where",
30
- " geo-ai-search-optimization scan <project-path> [--json]",
32
+ " geo-ai-search-optimization doctor [--json]",
33
+ " geo-ai-search-optimization init-llms [target-dir] [--site-name <name>] [--site-url <url>] [--overwrite] [--json]",
34
+ " geo-ai-search-optimization scan <project-path> [--json] [--out <file>] [--max-file-size <bytes>] [--max-examples <count>]",
31
35
  " geo-ai-search-optimization version",
32
36
  " geo-ai-search-optimization help",
33
37
  "",
@@ -40,6 +44,105 @@ function printHelp() {
40
44
  );
41
45
  }
42
46
 
47
+ function getFlagValue(args, flagName) {
48
+ const index = args.indexOf(flagName);
49
+ if (index === -1) {
50
+ return null;
51
+ }
52
+ if (index === args.length - 1) {
53
+ throw new Error(`${flagName} requires a value`);
54
+ }
55
+ return args[index + 1];
56
+ }
57
+
58
+ function hasFlag(args, ...flagNames) {
59
+ return flagNames.some((flagName) => args.includes(flagName));
60
+ }
61
+
62
+ function parsePositiveInteger(value, flagName) {
63
+ const parsed = Number.parseInt(value, 10);
64
+ if (!Number.isInteger(parsed) || parsed <= 0) {
65
+ throw new Error(`${flagName} must be a positive integer`);
66
+ }
67
+ return parsed;
68
+ }
69
+
70
+ async function handleInstall(args) {
71
+ const targetDir = getFlagValue(args, "--target");
72
+ const outputJson = hasFlag(args, "--json");
73
+ const result = await installSkill({ targetDir, silent: outputJson });
74
+
75
+ if (outputJson) {
76
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
77
+ }
78
+ }
79
+
80
+ function handleWhere() {
81
+ process.stdout.write(
82
+ [
83
+ `skill: ${getSkillName()}`,
84
+ `bundled: ${getBundledSkillPath()}`,
85
+ `skillsDir: ${getSkillsDir()}`,
86
+ `installed: ${getInstalledSkillPath()}`
87
+ ].join("\n") + "\n"
88
+ );
89
+ }
90
+
91
+ async function handleDoctor(args) {
92
+ const report = await runDoctor();
93
+ if (hasFlag(args, "--json")) {
94
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
95
+ return;
96
+ }
97
+ process.stdout.write(renderDoctorMarkdown(report));
98
+ }
99
+
100
+ async function handleScan(args) {
101
+ const projectPath = args.find((value) => !value.startsWith("-"));
102
+ if (!projectPath) {
103
+ throw new Error("scan requires a project path");
104
+ }
105
+
106
+ const maxFileSizeValue = getFlagValue(args, "--max-file-size");
107
+ const maxExamplesValue = getFlagValue(args, "--max-examples");
108
+ const summary = await scanProject(projectPath, {
109
+ maxFileSize: maxFileSizeValue ? parsePositiveInteger(maxFileSizeValue, "--max-file-size") : undefined,
110
+ maxExamples: maxExamplesValue ? parsePositiveInteger(maxExamplesValue, "--max-examples") : undefined
111
+ });
112
+
113
+ const outputJson = hasFlag(args, "--json");
114
+ const renderedOutput = outputJson
115
+ ? `${JSON.stringify(summary, null, 2)}\n`
116
+ : renderScanMarkdown(summary);
117
+
118
+ const outputPath = getFlagValue(args, "--out");
119
+ if (outputPath) {
120
+ const resolvedOutputPath = await writeScanOutput(outputPath, renderedOutput);
121
+ process.stdout.write(`Saved scan output to ${resolvedOutputPath}\n`);
122
+ return;
123
+ }
124
+
125
+ process.stdout.write(renderedOutput);
126
+ }
127
+
128
+ async function handleInitLlms(args) {
129
+ const targetDir = args.find((value) => !value.startsWith("-")) || ".";
130
+ const outputJson = hasFlag(args, "--json");
131
+ const result = await createLlmsTxt({
132
+ targetDir,
133
+ siteName: getFlagValue(args, "--site-name"),
134
+ siteUrl: getFlagValue(args, "--site-url"),
135
+ overwrite: hasFlag(args, "--overwrite")
136
+ });
137
+
138
+ if (outputJson) {
139
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
140
+ return;
141
+ }
142
+
143
+ process.stdout.write(`Created llms.txt at ${result.outputPath}\n`);
144
+ }
145
+
43
146
  export async function runCli(args = []) {
44
147
  const [command = "install", ...rest] = args;
45
148
 
@@ -54,35 +157,27 @@ export async function runCli(args = []) {
54
157
  }
55
158
 
56
159
  if (command === "install") {
57
- await installSkill();
160
+ await handleInstall(rest);
58
161
  return;
59
162
  }
60
163
 
61
164
  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
- );
165
+ handleWhere();
166
+ return;
167
+ }
168
+
169
+ if (command === "doctor") {
170
+ await handleDoctor(rest);
171
+ return;
172
+ }
173
+
174
+ if (command === "init-llms") {
175
+ await handleInitLlms(rest);
70
176
  return;
71
177
  }
72
178
 
73
179
  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
- }
180
+ await handleScan(rest);
86
181
  return;
87
182
  }
88
183
 
package/src/doctor.js ADDED
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs/promises";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { getBundledSkillPath, getInstalledSkillPath, getSkillName, getSkillsDir } from "./paths.js";
6
+
7
+ async function pathExists(targetPath) {
8
+ try {
9
+ await fs.access(targetPath);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ async function findExistingParent(targetPath) {
17
+ let current = path.resolve(targetPath);
18
+
19
+ while (true) {
20
+ if (await pathExists(current)) {
21
+ return current;
22
+ }
23
+
24
+ const parent = path.dirname(current);
25
+ if (parent === current) {
26
+ return current;
27
+ }
28
+ current = parent;
29
+ }
30
+ }
31
+
32
+ async function checkWritable(targetPath) {
33
+ const existingParent = await findExistingParent(targetPath);
34
+
35
+ try {
36
+ await fs.access(existingParent, fsConstants.W_OK);
37
+ return {
38
+ ok: true,
39
+ checkedPath: existingParent
40
+ };
41
+ } catch {
42
+ return {
43
+ ok: false,
44
+ checkedPath: existingParent
45
+ };
46
+ }
47
+ }
48
+
49
+ export async function runDoctor() {
50
+ const bundledPath = getBundledSkillPath();
51
+ const installedPath = getInstalledSkillPath();
52
+ const skillsDir = getSkillsDir();
53
+
54
+ const [bundledExists, installedExists, writableCheck] = await Promise.all([
55
+ pathExists(bundledPath),
56
+ pathExists(installedPath),
57
+ checkWritable(skillsDir)
58
+ ]);
59
+
60
+ return {
61
+ skill: getSkillName(),
62
+ nodeVersion: process.version,
63
+ platform: process.platform,
64
+ bundledPath,
65
+ installedPath,
66
+ skillsDir,
67
+ envOverrides: {
68
+ CODEX_HOME: process.env.CODEX_HOME || null,
69
+ CODEX_SKILLS_DIR: process.env.CODEX_SKILLS_DIR || null,
70
+ GEO_SKILL_INSTALL_DIR: process.env.GEO_SKILL_INSTALL_DIR || null
71
+ },
72
+ checks: {
73
+ bundledSkillPresent: bundledExists,
74
+ installedSkillPresent: installedExists,
75
+ skillsDirWritable: writableCheck.ok,
76
+ skillsDirWritableCheckPath: writableCheck.checkedPath
77
+ },
78
+ recommendations: buildDoctorRecommendations({
79
+ bundledExists,
80
+ installedExists,
81
+ writableCheck,
82
+ skillsDir
83
+ })
84
+ };
85
+ }
86
+
87
+ function buildDoctorRecommendations(context) {
88
+ const recommendations = [];
89
+
90
+ if (!context.bundledExists) {
91
+ recommendations.push("Bundled skill files are missing. Reinstall the npm package before using the CLI.");
92
+ }
93
+ if (!context.installedExists) {
94
+ recommendations.push("The skill is not installed into the Codex skills directory yet. Run `geo-ai-search-optimization install`.");
95
+ }
96
+ if (!context.writableCheck.ok) {
97
+ recommendations.push(
98
+ `The target skills directory is not writable from this environment. Use --target or set CODEX_SKILLS_DIR to a writable location.`
99
+ );
100
+ }
101
+ if (recommendations.length === 0) {
102
+ recommendations.push("Environment looks healthy. You can install, scan, and iterate on the skill from this machine.");
103
+ }
104
+
105
+ return recommendations;
106
+ }
107
+
108
+ export function renderDoctorMarkdown(report) {
109
+ const lines = [
110
+ "# GEO CLI Doctor",
111
+ "",
112
+ `- Skill: \`${report.skill}\``,
113
+ `- Node: \`${report.nodeVersion}\``,
114
+ `- Platform: \`${report.platform}\``,
115
+ `- Bundled path: \`${report.bundledPath}\``,
116
+ `- Installed path: \`${report.installedPath}\``,
117
+ `- Skills dir: \`${report.skillsDir}\``,
118
+ "",
119
+ "## Checks",
120
+ "",
121
+ `- bundledSkillPresent: \`${report.checks.bundledSkillPresent}\``,
122
+ `- installedSkillPresent: \`${report.checks.installedSkillPresent}\``,
123
+ `- skillsDirWritable: \`${report.checks.skillsDirWritable}\` via \`${report.checks.skillsDirWritableCheckPath}\``,
124
+ "",
125
+ "## Environment Overrides",
126
+ "",
127
+ `- CODEX_HOME: \`${report.envOverrides.CODEX_HOME ?? "unset"}\``,
128
+ `- CODEX_SKILLS_DIR: \`${report.envOverrides.CODEX_SKILLS_DIR ?? "unset"}\``,
129
+ `- GEO_SKILL_INSTALL_DIR: \`${report.envOverrides.GEO_SKILL_INSTALL_DIR ?? "unset"}\``,
130
+ "",
131
+ "## Recommendations",
132
+ ""
133
+ ];
134
+
135
+ for (const recommendation of report.recommendations) {
136
+ lines.push(`- ${recommendation}`);
137
+ }
138
+
139
+ return `${lines.join("\n")}\n`;
140
+ }
package/src/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { installSkill } from "./install-skill.js";
2
2
  export { runCli } from "./cli.js";
3
- export { scanProject, renderScanMarkdown } from "./scan.js";
3
+ export { runDoctor, renderDoctorMarkdown } from "./doctor.js";
4
+ export { createLlmsTxt } from "./llms-txt.js";
5
+ export { scanProject, renderScanMarkdown, writeScanOutput } from "./scan.js";
@@ -28,7 +28,8 @@ export async function installSkill(options = {}) {
28
28
  skill: getSkillName(),
29
29
  installedAt: new Date().toISOString(),
30
30
  installedFrom: sourceDir,
31
- packageName: "geo-ai-search-optimization"
31
+ packageName: "geo-ai-search-optimization",
32
+ targetDir
32
33
  };
33
34
 
34
35
  await fs.writeFile(
@@ -44,6 +45,7 @@ export async function installSkill(options = {}) {
44
45
  return {
45
46
  targetDir,
46
47
  sourceDir,
47
- skillName: getSkillName()
48
+ skillName: getSkillName(),
49
+ manifest
48
50
  };
49
51
  }
@@ -0,0 +1,76 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ async function pathExists(targetPath) {
5
+ try {
6
+ await fs.access(targetPath);
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ function buildLlmsTxtContent({ siteName, siteUrl }) {
14
+ const resolvedSiteName = siteName || "Your Site Name";
15
+ const resolvedSiteUrl = siteUrl || "https://example.com";
16
+
17
+ return [
18
+ `# ${resolvedSiteName}`,
19
+ "",
20
+ `> GEO-first site profile for AI assistants and answer engines.`,
21
+ "",
22
+ `Canonical: ${resolvedSiteUrl}`,
23
+ "",
24
+ "## About",
25
+ "",
26
+ "- Summarize what this site does in 1-2 sentences.",
27
+ "- Describe the primary audience and core use cases.",
28
+ "- Mention what makes the content or product uniquely trustworthy.",
29
+ "",
30
+ "## Priority URLs",
31
+ "",
32
+ `- ${resolvedSiteUrl}/`,
33
+ `- ${resolvedSiteUrl}/pricing`,
34
+ `- ${resolvedSiteUrl}/docs`,
35
+ `- ${resolvedSiteUrl}/blog`,
36
+ "",
37
+ "## Recommended Sources",
38
+ "",
39
+ "- Link to pages with first-party research, methodology, benchmarks, or clear product facts.",
40
+ "- Prefer canonical pages over campaign or temporary URLs.",
41
+ "",
42
+ "## AI Assistant Guidance",
43
+ "",
44
+ "- Use the latest canonical URLs when citing this site.",
45
+ "- Prefer pages with explicit dates, authors, reviewers, or methodology notes.",
46
+ "- Avoid summarizing outdated pages when newer versions exist.",
47
+ "",
48
+ "## Update Policy",
49
+ "",
50
+ "- Replace this section with the site’s actual content freshness and review cadence.",
51
+ ""
52
+ ].join("\n");
53
+ }
54
+
55
+ export async function createLlmsTxt(options = {}) {
56
+ const targetDir = path.resolve(options.targetDir || ".");
57
+ const outputPath = path.join(targetDir, "llms.txt");
58
+ const overwrite = Boolean(options.overwrite);
59
+
60
+ if (!overwrite && (await pathExists(outputPath))) {
61
+ throw new Error(`llms.txt already exists at ${outputPath}. Use --overwrite to replace it.`);
62
+ }
63
+
64
+ await fs.mkdir(targetDir, { recursive: true });
65
+ const content = buildLlmsTxtContent({
66
+ siteName: options.siteName,
67
+ siteUrl: options.siteUrl
68
+ });
69
+ await fs.writeFile(outputPath, content, "utf8");
70
+
71
+ return {
72
+ outputPath,
73
+ siteName: options.siteName || "Your Site Name",
74
+ siteUrl: options.siteUrl || "https://example.com"
75
+ };
76
+ }
package/src/scan.js CHANGED
@@ -207,6 +207,13 @@ export async function scanProject(rootInput, options = {}) {
207
207
  return summary;
208
208
  }
209
209
 
210
+ export async function writeScanOutput(outputPath, content) {
211
+ const resolvedPath = path.resolve(outputPath);
212
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
213
+ await fs.writeFile(resolvedPath, content, "utf8");
214
+ return resolvedPath;
215
+ }
216
+
210
217
  export function renderScanMarkdown(summary) {
211
218
  const lines = [
212
219
  "# GEO Signal Scan",