aeo-ready 1.1.0 → 1.3.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/README.md +52 -76
- package/bin/cli.js +20 -62
- package/package.json +2 -2
- package/src/benchmark/agentic-seo.js +107 -6
- package/src/benchmark/index.js +2 -2
- package/src/dashboard/generate.js +22 -48
- package/src/dashboard/sections/benchmark-details.js +79 -0
- package/src/dashboard/sections/history-table.js +10 -7
- package/src/dashboard/sections/overall-score.js +55 -118
- package/src/dashboard/sections/trend-chart.js +31 -46
- package/src/history/index.js +8 -15
- package/src/scan.js +59 -293
- package/src/checks/agent-readiness/actionable.js +0 -165
- package/src/checks/agent-readiness/capability.js +0 -209
- package/src/checks/agent-readiness/content-structure.js +0 -242
- package/src/checks/agent-readiness/discovery.js +0 -231
- package/src/checks/ai-visibility/authority.js +0 -195
- package/src/checks/ai-visibility/citation-readiness.js +0 -228
- package/src/checks/ai-visibility/freshness.js +0 -182
- package/src/checks/ai-visibility/structured-data.js +0 -180
- package/src/dashboard/sections/agent-readiness.js +0 -71
- package/src/dashboard/sections/ai-visibility.js +0 -67
- package/src/dashboard/sections/recommendations.js +0 -196
- package/src/fix/generators/agents-json.js +0 -73
- package/src/fix/generators/agents-md.js +0 -85
- package/src/fix/generators/llms-txt.js +0 -166
- package/src/fix/generators/robots-txt.js +0 -64
- package/src/fix/index.js +0 -177
- package/src/track/index.js +0 -167
- package/src/utils/detect-type.js +0 -99
- package/src/utils/tokens.js +0 -18
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
|
|
4
|
-
export function generateLlmsTxt(check, scanResult, dir) {
|
|
5
|
-
const { siteType, target } = scanResult;
|
|
6
|
-
const hostname = extractHostname(target);
|
|
7
|
-
const name = inferName(dir, hostname);
|
|
8
|
-
|
|
9
|
-
const content = generateByType(siteType, name, target);
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
file: "llms.txt",
|
|
13
|
-
description: `Draft llms.txt for ${siteType} site — REVIEW AND EDIT before publishing`,
|
|
14
|
-
draft: true,
|
|
15
|
-
content,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function generateByType(siteType, name, target) {
|
|
20
|
-
const base = target || "https://example.com";
|
|
21
|
-
|
|
22
|
-
switch (siteType) {
|
|
23
|
-
case "saas":
|
|
24
|
-
return `# ${name || "[Company Name]"}
|
|
25
|
-
|
|
26
|
-
> [One sentence: what problem you solve and for whom]
|
|
27
|
-
|
|
28
|
-
## What it does
|
|
29
|
-
- [Core capability 1]
|
|
30
|
-
- [Core capability 2]
|
|
31
|
-
- [Core capability 3]
|
|
32
|
-
|
|
33
|
-
## Pricing
|
|
34
|
-
- [Plan 1]: $X/mo — [what's included]
|
|
35
|
-
- [Plan 2]: $X/mo — [what's included]
|
|
36
|
-
- Details: ${base}/pricing
|
|
37
|
-
|
|
38
|
-
## Integration
|
|
39
|
-
- API docs: ${base}/docs
|
|
40
|
-
- OpenAPI spec: ${base}/openapi.json
|
|
41
|
-
- Auth: [API key / OAuth / etc]
|
|
42
|
-
|
|
43
|
-
## Get started
|
|
44
|
-
1. [Signup step]
|
|
45
|
-
2. [First API call or setup]
|
|
46
|
-
3. [See results]
|
|
47
|
-
|
|
48
|
-
## Links
|
|
49
|
-
- Documentation: ${base}/docs
|
|
50
|
-
- Changelog: ${base}/changelog
|
|
51
|
-
- Status: ${base}/status
|
|
52
|
-
`;
|
|
53
|
-
|
|
54
|
-
case "api":
|
|
55
|
-
return `# ${name || "[Tool Name]"}
|
|
56
|
-
|
|
57
|
-
> [One sentence: what this tool does]
|
|
58
|
-
|
|
59
|
-
## Quick start
|
|
60
|
-
\`\`\`bash
|
|
61
|
-
[install command]
|
|
62
|
-
\`\`\`
|
|
63
|
-
|
|
64
|
-
## Key concepts
|
|
65
|
-
- [Concept 1]: [one-line explanation]
|
|
66
|
-
- [Concept 2]: [one-line explanation]
|
|
67
|
-
|
|
68
|
-
## API reference
|
|
69
|
-
- Base URL: ${base}/api/v1
|
|
70
|
-
- Auth: [method]
|
|
71
|
-
- OpenAPI: ${base}/openapi.json
|
|
72
|
-
|
|
73
|
-
## SDKs
|
|
74
|
-
- Node.js: \`npm install [package]\`
|
|
75
|
-
- Python: \`pip install [package]\`
|
|
76
|
-
|
|
77
|
-
## Links
|
|
78
|
-
- Full docs: ${base}/docs
|
|
79
|
-
- Examples: ${base}/examples
|
|
80
|
-
- Changelog: ${base}/changelog
|
|
81
|
-
`;
|
|
82
|
-
|
|
83
|
-
case "personal":
|
|
84
|
-
return `# ${name || "[Your Name]"}
|
|
85
|
-
|
|
86
|
-
> [Role/title] — [what you're known for]
|
|
87
|
-
|
|
88
|
-
## Expertise
|
|
89
|
-
- [Area 1]
|
|
90
|
-
- [Area 2]
|
|
91
|
-
- [Area 3]
|
|
92
|
-
|
|
93
|
-
## Notable work
|
|
94
|
-
- [Project 1]: [one-line description] — [url]
|
|
95
|
-
- [Project 2]: [one-line description] — [url]
|
|
96
|
-
|
|
97
|
-
## Writing & speaking
|
|
98
|
-
- [Topic you cover]
|
|
99
|
-
- [Where to find your content]
|
|
100
|
-
|
|
101
|
-
## Contact
|
|
102
|
-
- Email: [email]
|
|
103
|
-
- LinkedIn: [url]
|
|
104
|
-
- Available for: [consulting / speaking / hiring / etc]
|
|
105
|
-
`;
|
|
106
|
-
|
|
107
|
-
case "content":
|
|
108
|
-
return `# ${name || "[Publication Name]"}
|
|
109
|
-
|
|
110
|
-
> [What topics you cover and your angle/expertise]
|
|
111
|
-
|
|
112
|
-
## Topics
|
|
113
|
-
- [Topic 1]: [what you cover, how many pieces]
|
|
114
|
-
- [Topic 2]: [what you cover, how many pieces]
|
|
115
|
-
|
|
116
|
-
## Best starting points
|
|
117
|
-
- [Most important article]: ${base}/[path]
|
|
118
|
-
- [Second article]: ${base}/[path]
|
|
119
|
-
- [Third article]: ${base}/[path]
|
|
120
|
-
|
|
121
|
-
## About
|
|
122
|
-
- Author(s): [who writes]
|
|
123
|
-
- Cadence: [how often you publish]
|
|
124
|
-
- Angle: [what makes your coverage unique]
|
|
125
|
-
|
|
126
|
-
## Links
|
|
127
|
-
- All articles: ${base}/blog
|
|
128
|
-
- RSS: ${base}/feed.xml
|
|
129
|
-
- Newsletter: ${base}/subscribe
|
|
130
|
-
`;
|
|
131
|
-
|
|
132
|
-
default:
|
|
133
|
-
return `# ${name || "[Site Name]"}
|
|
134
|
-
|
|
135
|
-
> [One sentence: what this site is and who it's for]
|
|
136
|
-
|
|
137
|
-
## What you'll find here
|
|
138
|
-
- [Section 1]: [description]
|
|
139
|
-
- [Section 2]: [description]
|
|
140
|
-
|
|
141
|
-
## Links
|
|
142
|
-
- Homepage: ${base}
|
|
143
|
-
`;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function extractHostname(target) {
|
|
148
|
-
if (!target) return null;
|
|
149
|
-
try {
|
|
150
|
-
return new URL(target).hostname.replace("www.", "");
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function inferName(dir, hostname) {
|
|
157
|
-
const pkgPath = join(dir, "package.json");
|
|
158
|
-
if (existsSync(pkgPath)) {
|
|
159
|
-
try {
|
|
160
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
161
|
-
if (pkg.name) return pkg.name;
|
|
162
|
-
} catch {}
|
|
163
|
-
}
|
|
164
|
-
if (hostname) return hostname.split(".")[0];
|
|
165
|
-
return "";
|
|
166
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
|
|
4
|
-
const AI_CRAWLERS = [
|
|
5
|
-
"GPTBot",
|
|
6
|
-
"OAI-SearchBot",
|
|
7
|
-
"ClaudeBot",
|
|
8
|
-
"Claude-User",
|
|
9
|
-
"Claude-SearchBot",
|
|
10
|
-
"Google-Extended",
|
|
11
|
-
"PerplexityBot",
|
|
12
|
-
"Meta-ExternalAgent",
|
|
13
|
-
"CCBot",
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
export function generateRobotsTxt(check, scanResult, dir) {
|
|
17
|
-
const filePath = join(dir, "robots.txt");
|
|
18
|
-
const exists = existsSync(filePath);
|
|
19
|
-
|
|
20
|
-
if (exists) {
|
|
21
|
-
return {
|
|
22
|
-
file: "robots.txt",
|
|
23
|
-
description: "Add missing AI crawler rules",
|
|
24
|
-
draft: false,
|
|
25
|
-
merge: mergeRobotsTxt,
|
|
26
|
-
content: generateCrawlerBlock(),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
file: "robots.txt",
|
|
32
|
-
description: "Allow all AI crawlers for maximum agent discoverability",
|
|
33
|
-
draft: false,
|
|
34
|
-
content: generateFullRobotsTxt(),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function mergeRobotsTxt(existing, newContent) {
|
|
39
|
-
const presentBots = AI_CRAWLERS.filter((bot) => existing.includes(bot));
|
|
40
|
-
const missingBots = AI_CRAWLERS.filter((bot) => !existing.includes(bot));
|
|
41
|
-
|
|
42
|
-
if (missingBots.length === 0) return existing;
|
|
43
|
-
|
|
44
|
-
const additions = missingBots
|
|
45
|
-
.map((bot) => `User-agent: ${bot}\nAllow: /\n`)
|
|
46
|
-
.join("\n");
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
existing.trimEnd() + "\n\n# AI crawlers (added by agent-web)\n" + additions
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function generateCrawlerBlock() {
|
|
54
|
-
return AI_CRAWLERS.map((bot) => `User-agent: ${bot}\nAllow: /\n`).join("\n");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function generateFullRobotsTxt() {
|
|
58
|
-
let content = "User-agent: *\nAllow: /\n\n";
|
|
59
|
-
content += "# AI crawlers — explicit allow for agent discoverability\n";
|
|
60
|
-
content += AI_CRAWLERS.map((bot) => `User-agent: ${bot}\nAllow: /\n`).join(
|
|
61
|
-
"\n",
|
|
62
|
-
);
|
|
63
|
-
return content;
|
|
64
|
-
}
|
package/src/fix/index.js
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { writeFileSync, existsSync, readFileSync, mkdirSync } from "fs";
|
|
3
|
-
import { join, dirname } from "path";
|
|
4
|
-
import { generateRobotsTxt } from "./generators/robots-txt.js";
|
|
5
|
-
import { generateAgentsJson } from "./generators/agents-json.js";
|
|
6
|
-
import { generateLlmsTxt } from "./generators/llms-txt.js";
|
|
7
|
-
import { generateAgentsMd } from "./generators/agents-md.js";
|
|
8
|
-
import { generateDashboard } from "../dashboard/generate.js";
|
|
9
|
-
|
|
10
|
-
export async function fix(beforeResult, dir, rescanFn) {
|
|
11
|
-
const failedChecks = getAllFailedChecks(beforeResult);
|
|
12
|
-
const applied = [];
|
|
13
|
-
const manual = [];
|
|
14
|
-
|
|
15
|
-
for (const check of failedChecks) {
|
|
16
|
-
const result = await generateFix(check, beforeResult, dir);
|
|
17
|
-
if (result) {
|
|
18
|
-
if (result.draft) {
|
|
19
|
-
manual.push(result);
|
|
20
|
-
} else {
|
|
21
|
-
writeFix(result, dir);
|
|
22
|
-
applied.push(result);
|
|
23
|
-
}
|
|
24
|
-
} else {
|
|
25
|
-
manual.push({ description: check.fix, name: check.name });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const afterResult = rescanFn ? await rescanFn() : null;
|
|
30
|
-
|
|
31
|
-
printSummary(beforeResult, afterResult, applied, manual);
|
|
32
|
-
|
|
33
|
-
if (afterResult && dir) {
|
|
34
|
-
const dashPath = await generateDashboard(afterResult, dir, {
|
|
35
|
-
beforeResult,
|
|
36
|
-
});
|
|
37
|
-
console.log(chalk.dim(` Dashboard: ${dashPath}\n`));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { applied, manual, afterResult };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function printSummary(before, after, applied, manual) {
|
|
44
|
-
const beforeScore = before.score;
|
|
45
|
-
const afterScore = after ? after.score : null;
|
|
46
|
-
|
|
47
|
-
console.log(chalk.bold("\n aeo-ready --fix\n"));
|
|
48
|
-
|
|
49
|
-
if (afterScore !== null) {
|
|
50
|
-
const delta = afterScore - beforeScore;
|
|
51
|
-
const deltaStr =
|
|
52
|
-
delta > 0
|
|
53
|
-
? chalk.green(` (+${delta})`)
|
|
54
|
-
: delta < 0
|
|
55
|
-
? chalk.red(` (${delta})`)
|
|
56
|
-
: "";
|
|
57
|
-
const beforeGrade = gradeColor(before.grade)(`${beforeScore}`);
|
|
58
|
-
const afterGrade = gradeColor(after.grade)(`${afterScore}`);
|
|
59
|
-
console.log(
|
|
60
|
-
` ${beforeGrade}${chalk.dim("/100")} ${chalk.dim("->")} ${afterGrade}${chalk.dim("/100")}${deltaStr}\n`,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const bA = before.agentReadiness.score;
|
|
64
|
-
const bV = before.aiVisibility.score;
|
|
65
|
-
const aA = after.agentReadiness.score;
|
|
66
|
-
const aV = after.aiVisibility.score;
|
|
67
|
-
const dA = aA - bA;
|
|
68
|
-
const dV = aV - bV;
|
|
69
|
-
const fmtDelta = (d) =>
|
|
70
|
-
d > 0 ? chalk.green(`+${d}`) : d < 0 ? chalk.red(`${d}`) : chalk.dim("—");
|
|
71
|
-
|
|
72
|
-
console.log(
|
|
73
|
-
chalk.dim(" Agent Readiness ") +
|
|
74
|
-
`${bA} -> ${aA} ${fmtDelta(dA)}` +
|
|
75
|
-
chalk.dim(" can agents find, read, and use your site?"),
|
|
76
|
-
);
|
|
77
|
-
console.log(
|
|
78
|
-
chalk.dim(" AI Visibility ") +
|
|
79
|
-
`${bV} -> ${aV} ${fmtDelta(dV)}` +
|
|
80
|
-
chalk.dim(" how accurately do AI engines describe you?"),
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
if (bA < 25) {
|
|
84
|
-
console.log(
|
|
85
|
-
chalk.dim(
|
|
86
|
-
"\n Agent Readiness is low — AI engines can't cite what they can't read.",
|
|
87
|
-
),
|
|
88
|
-
);
|
|
89
|
-
console.log(chalk.dim(" Fix the agent side first. Visibility follows."));
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
console.log(
|
|
93
|
-
` ${chalk.dim("Score:")} ${beforeScore}/100 (${before.grade})\n`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (applied.length > 0) {
|
|
98
|
-
console.log(chalk.bold("\n We fixed:"));
|
|
99
|
-
for (const fix of applied) {
|
|
100
|
-
console.log(
|
|
101
|
-
chalk.green(` + ${fix.file}`) + chalk.dim(` — ${fix.description}`),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const humanTasks = manual.filter((m) => m.description);
|
|
107
|
-
if (humanTasks.length > 0) {
|
|
108
|
-
console.log(chalk.bold("\n You still need to:"));
|
|
109
|
-
for (const task of humanTasks.slice(0, 6)) {
|
|
110
|
-
console.log(chalk.dim(` - ${task.description}`));
|
|
111
|
-
}
|
|
112
|
-
if (humanTasks.length > 6) {
|
|
113
|
-
console.log(chalk.dim(` ... and ${humanTasks.length - 6} more`));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
console.log("");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function writeFix(fix, dir) {
|
|
121
|
-
const filePath = join(dir, fix.file);
|
|
122
|
-
const fileDir = dirname(filePath);
|
|
123
|
-
if (!existsSync(fileDir)) mkdirSync(fileDir, { recursive: true });
|
|
124
|
-
|
|
125
|
-
if (existsSync(filePath) && fix.merge) {
|
|
126
|
-
const existing = readFileSync(filePath, "utf8");
|
|
127
|
-
const merged = fix.merge(existing, fix.content);
|
|
128
|
-
writeFileSync(filePath, merged);
|
|
129
|
-
} else {
|
|
130
|
-
writeFileSync(filePath, fix.content);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function generateFix(check, scanResult, dir) {
|
|
135
|
-
switch (check.name) {
|
|
136
|
-
case "robots.txt AI crawlers":
|
|
137
|
-
return generateRobotsTxt(check, scanResult, dir);
|
|
138
|
-
case "agents.json manifest":
|
|
139
|
-
return generateAgentsJson(check, scanResult, dir);
|
|
140
|
-
case "AGENTS.md / CLAUDE.md":
|
|
141
|
-
return generateAgentsMd(check, scanResult, dir);
|
|
142
|
-
case "llms.txt":
|
|
143
|
-
return generateLlmsTxt(check, scanResult, dir);
|
|
144
|
-
default:
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function getAllFailedChecks(scanResult) {
|
|
150
|
-
const checks = [];
|
|
151
|
-
for (const scorecard of [
|
|
152
|
-
scanResult.agentReadiness,
|
|
153
|
-
scanResult.aiVisibility,
|
|
154
|
-
]) {
|
|
155
|
-
for (const cat of Object.values(scorecard.categories)) {
|
|
156
|
-
for (const check of cat.checks) {
|
|
157
|
-
if (!check.passed) {
|
|
158
|
-
checks.push(check);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return checks;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function gradeColor(grade) {
|
|
167
|
-
switch (grade) {
|
|
168
|
-
case "A":
|
|
169
|
-
return chalk.green;
|
|
170
|
-
case "B":
|
|
171
|
-
return chalk.cyan;
|
|
172
|
-
case "C":
|
|
173
|
-
return chalk.yellow;
|
|
174
|
-
default:
|
|
175
|
-
return chalk.red;
|
|
176
|
-
}
|
|
177
|
-
}
|
package/src/track/index.js
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
|
|
3
|
-
const PROVIDERS = {
|
|
4
|
-
claude: {
|
|
5
|
-
name: "Claude",
|
|
6
|
-
url: "https://api.anthropic.com/v1/messages",
|
|
7
|
-
envKey: "ANTHROPIC_API_KEY",
|
|
8
|
-
model: "claude-sonnet-4-6",
|
|
9
|
-
call: callClaude,
|
|
10
|
-
},
|
|
11
|
-
openai: {
|
|
12
|
-
name: "ChatGPT",
|
|
13
|
-
url: "https://api.openai.com/v1/chat/completions",
|
|
14
|
-
envKey: "OPENAI_API_KEY",
|
|
15
|
-
model: "gpt-4o-mini",
|
|
16
|
-
call: callOpenAI,
|
|
17
|
-
},
|
|
18
|
-
google: {
|
|
19
|
-
name: "Gemini",
|
|
20
|
-
envKey: "GOOGLE_API_KEY",
|
|
21
|
-
model: "gemini-2.0-flash",
|
|
22
|
-
call: callGoogle,
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const DEFAULT_PROMPTS = [
|
|
27
|
-
{ template: "What is {company}?", category: "brand" },
|
|
28
|
-
{ template: "What does {company} do?", category: "brand" },
|
|
29
|
-
{ template: "Best {category} tools?", category: "discovery" },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
export async function track(scanResult, config) {
|
|
33
|
-
const company = config.company || inferCompany(scanResult);
|
|
34
|
-
const category = config.category || "";
|
|
35
|
-
const prompts = config.prompts || DEFAULT_PROMPTS;
|
|
36
|
-
|
|
37
|
-
const available = getAvailableProviders();
|
|
38
|
-
if (available.length === 0) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const rendered = prompts.map((p) => ({
|
|
43
|
-
...p,
|
|
44
|
-
text: p.template
|
|
45
|
-
.replace("{company}", company)
|
|
46
|
-
.replace("{category}", category),
|
|
47
|
-
}));
|
|
48
|
-
|
|
49
|
-
const results = [];
|
|
50
|
-
|
|
51
|
-
for (const prompt of rendered) {
|
|
52
|
-
for (const provider of available) {
|
|
53
|
-
try {
|
|
54
|
-
const response = await provider.call(prompt.text, provider.model);
|
|
55
|
-
results.push({
|
|
56
|
-
provider: provider.name,
|
|
57
|
-
prompt: prompt.text,
|
|
58
|
-
category: prompt.category,
|
|
59
|
-
response: response.slice(0, 500),
|
|
60
|
-
mentions: response.toLowerCase().includes(company.toLowerCase()),
|
|
61
|
-
});
|
|
62
|
-
} catch {
|
|
63
|
-
results.push({
|
|
64
|
-
provider: provider.name,
|
|
65
|
-
prompt: prompt.text,
|
|
66
|
-
category: prompt.category,
|
|
67
|
-
response: "(failed)",
|
|
68
|
-
mentions: false,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return results;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function printTrackResults(results, company) {
|
|
78
|
-
if (!results || results.length === 0) return;
|
|
79
|
-
|
|
80
|
-
console.log(chalk.bold("\n AI Visibility — what models say about you\n"));
|
|
81
|
-
|
|
82
|
-
const grouped = {};
|
|
83
|
-
for (const r of results) {
|
|
84
|
-
if (!grouped[r.prompt]) grouped[r.prompt] = [];
|
|
85
|
-
grouped[r.prompt].push(r);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
for (const [prompt, responses] of Object.entries(grouped)) {
|
|
89
|
-
console.log(chalk.dim(` > "${prompt}"\n`));
|
|
90
|
-
for (const r of responses) {
|
|
91
|
-
const icon = r.mentions ? chalk.green("cited") : chalk.red("not cited");
|
|
92
|
-
console.log(` ${chalk.bold(r.provider)} [${icon}]`);
|
|
93
|
-
const lines = r.response.split("\n").slice(0, 3);
|
|
94
|
-
for (const line of lines) {
|
|
95
|
-
console.log(chalk.dim(` ${line.slice(0, 80)}`));
|
|
96
|
-
}
|
|
97
|
-
console.log("");
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const total = results.length;
|
|
102
|
-
const cited = results.filter((r) => r.mentions).length;
|
|
103
|
-
const rate = Math.round((cited / total) * 100);
|
|
104
|
-
console.log(chalk.bold(` Citation rate: ${cited}/${total} (${rate}%)\n`));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function getAvailableProviders() {
|
|
108
|
-
return Object.values(PROVIDERS).filter((p) => process.env[p.envKey]);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function inferCompany(scanResult) {
|
|
112
|
-
const target = scanResult.target || "";
|
|
113
|
-
try {
|
|
114
|
-
return new URL(target).hostname.replace("www.", "").split(".")[0];
|
|
115
|
-
} catch {
|
|
116
|
-
return target.split("/").pop() || "unknown";
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function callClaude(prompt, model) {
|
|
121
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers: {
|
|
124
|
-
"x-api-key": process.env.ANTHROPIC_API_KEY,
|
|
125
|
-
"anthropic-version": "2023-06-01",
|
|
126
|
-
"content-type": "application/json",
|
|
127
|
-
},
|
|
128
|
-
body: JSON.stringify({
|
|
129
|
-
model,
|
|
130
|
-
max_tokens: 300,
|
|
131
|
-
messages: [{ role: "user", content: prompt }],
|
|
132
|
-
}),
|
|
133
|
-
});
|
|
134
|
-
const data = await res.json();
|
|
135
|
-
return data.content?.[0]?.text || "";
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function callOpenAI(prompt, model) {
|
|
139
|
-
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
140
|
-
method: "POST",
|
|
141
|
-
headers: {
|
|
142
|
-
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
|
|
143
|
-
"content-type": "application/json",
|
|
144
|
-
},
|
|
145
|
-
body: JSON.stringify({
|
|
146
|
-
model,
|
|
147
|
-
messages: [{ role: "user", content: prompt }],
|
|
148
|
-
max_tokens: 300,
|
|
149
|
-
}),
|
|
150
|
-
});
|
|
151
|
-
const data = await res.json();
|
|
152
|
-
return data.choices?.[0]?.message?.content || "";
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function callGoogle(prompt, model) {
|
|
156
|
-
const key = process.env.GOOGLE_API_KEY;
|
|
157
|
-
const res = await fetch(
|
|
158
|
-
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`,
|
|
159
|
-
{
|
|
160
|
-
method: "POST",
|
|
161
|
-
headers: { "content-type": "application/json" },
|
|
162
|
-
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
|
|
163
|
-
},
|
|
164
|
-
);
|
|
165
|
-
const data = await res.json();
|
|
166
|
-
return data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
167
|
-
}
|
package/src/utils/detect-type.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
export function detectSiteType(signals) {
|
|
2
|
-
const { html, files, url } = signals;
|
|
3
|
-
const text = (html || "").toLowerCase();
|
|
4
|
-
const fileList = (files || []).map((f) => f.toLowerCase());
|
|
5
|
-
|
|
6
|
-
const scores = {
|
|
7
|
-
saas: scoreSaas(text, fileList, url),
|
|
8
|
-
api: scoreApi(text, fileList, url),
|
|
9
|
-
content: scoreContent(text, fileList, url),
|
|
10
|
-
personal: scorePersonal(text, fileList, url),
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
|
14
|
-
if (sorted[0][1] === 0) return "unknown";
|
|
15
|
-
return sorted[0][0];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function scoreSaas(text, files, url) {
|
|
19
|
-
let score = 0;
|
|
20
|
-
if (text.includes("/pricing") || text.includes("/plans")) score += 3;
|
|
21
|
-
if (text.includes("pricing") && !text.includes("/pricing")) score += 1;
|
|
22
|
-
if (text.includes("sign up") || text.includes("signup")) score += 2;
|
|
23
|
-
if (text.includes("free trial") || text.includes("start free")) score += 2;
|
|
24
|
-
if (text.includes("dashboard") && text.includes("login")) score += 2;
|
|
25
|
-
if (text.includes("per month") || text.includes("/mo")) score += 2;
|
|
26
|
-
if (files.some((f) => f.includes("pricing"))) score += 2;
|
|
27
|
-
return score;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function scoreApi(text, files, url) {
|
|
31
|
-
let score = 0;
|
|
32
|
-
const hostname = extractHostname(url);
|
|
33
|
-
if (
|
|
34
|
-
hostname &&
|
|
35
|
-
(hostname.startsWith("docs.") || hostname.startsWith("developer."))
|
|
36
|
-
)
|
|
37
|
-
score += 4;
|
|
38
|
-
if (text.includes("api reference") || text.includes("api docs")) score += 3;
|
|
39
|
-
if (text.includes("sdk") || text.includes("client library")) score += 2;
|
|
40
|
-
if (text.includes("openapi") || text.includes("swagger")) score += 3;
|
|
41
|
-
if (text.includes("endpoint") || text.includes("authentication")) score += 1;
|
|
42
|
-
if (text.includes("npm install") || text.includes("pip install")) score += 2;
|
|
43
|
-
if (files.some((f) => f.includes("openapi") || f.includes("swagger")))
|
|
44
|
-
score += 3;
|
|
45
|
-
if (files.some((f) => f.includes("sdk") || f.includes("client"))) score += 1;
|
|
46
|
-
return score;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function scoreContent(text, files, url) {
|
|
50
|
-
let score = 0;
|
|
51
|
-
if (text.includes("/blog") || text.includes("blog")) score += 2;
|
|
52
|
-
if ((text.match(/article/g) || []).length >= 3) score += 2;
|
|
53
|
-
if (text.includes("published") && text.includes("author")) score += 2;
|
|
54
|
-
if (text.includes("read more") || text.includes("continue reading"))
|
|
55
|
-
score += 2;
|
|
56
|
-
if (text.includes("subscribe") && text.includes("newsletter")) score += 1;
|
|
57
|
-
if (files.some((f) => f.includes("blog") || f.includes("posts"))) score += 3;
|
|
58
|
-
if (files.filter((f) => f.endsWith(".md")).length > 10) score += 1;
|
|
59
|
-
return score;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function scorePersonal(text, files, url) {
|
|
63
|
-
let score = 0;
|
|
64
|
-
const hostname = extractHostname(url);
|
|
65
|
-
if (hostname && isPersonalDomain(hostname)) score += 3;
|
|
66
|
-
if (text.includes("portfolio") || text.includes("my work")) score += 3;
|
|
67
|
-
if (
|
|
68
|
-
text.includes("about me") ||
|
|
69
|
-
text.includes("i am") ||
|
|
70
|
-
text.includes("i'm a")
|
|
71
|
-
)
|
|
72
|
-
score += 3;
|
|
73
|
-
if (text.includes("resume") || text.includes("cv")) score += 2;
|
|
74
|
-
if (text.includes("hire me") || text.includes("available for")) score += 2;
|
|
75
|
-
if (text.includes("linkedin.com/in/") || text.includes("github.com/"))
|
|
76
|
-
score += 1;
|
|
77
|
-
if (text.includes("@") && text.includes("contact")) score += 1;
|
|
78
|
-
return score;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function extractHostname(url) {
|
|
82
|
-
if (!url) return null;
|
|
83
|
-
try {
|
|
84
|
-
return new URL(url).hostname.replace("www.", "");
|
|
85
|
-
} catch {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isPersonalDomain(hostname) {
|
|
91
|
-
const parts = hostname.split(".");
|
|
92
|
-
const name = parts[0];
|
|
93
|
-
const tld = parts.slice(1).join(".");
|
|
94
|
-
const personalTlds = ["com", "me", "io", "dev", "co", "net", "org"];
|
|
95
|
-
if (!personalTlds.includes(tld)) return false;
|
|
96
|
-
return /^[a-z]+[a-z]+$/.test(name) && name.length >= 6 && name.length <= 20;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export const SITE_TYPES = ["saas", "api", "content", "personal", "unknown"];
|
package/src/utils/tokens.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export function estimateTokens(text) {
|
|
2
|
-
if (!text) return 0;
|
|
3
|
-
return Math.ceil(text.length / 4);
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export const TOKEN_BUDGETS = {
|
|
7
|
-
quickstart: 15000,
|
|
8
|
-
apiRef: 25000,
|
|
9
|
-
guide: 20000,
|
|
10
|
-
llmsTxt: 5000,
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function checkTokenBudget(text, type) {
|
|
14
|
-
const tokens = estimateTokens(text);
|
|
15
|
-
const budget = TOKEN_BUDGETS[type];
|
|
16
|
-
if (!budget) return { tokens, withinBudget: true, budget: null };
|
|
17
|
-
return { tokens, withinBudget: tokens <= budget, budget };
|
|
18
|
-
}
|