seomd-cli 1.0.2 ā 1.1.1
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/bin/seomd.js +1 -0
- package/package.json +2 -2
- package/src/commands/analyze.js +44 -9
- package/src/commands/sync.js +2 -1
- package/src/utils/api-client.js +18 -1
- package/src/utils/writeback.js +3 -3
package/bin/seomd.js
CHANGED
|
@@ -32,6 +32,7 @@ program
|
|
|
32
32
|
.description('Run citation analysis and write back _analysis blocks')
|
|
33
33
|
.option('--page <url>', 'analyze a specific page only')
|
|
34
34
|
.option('--intent <category>', 'analyze a specific intent category only')
|
|
35
|
+
.option('--engines <list>', 'comma-separated list of engines to scan (e.g. chatgpt,claude)')
|
|
35
36
|
.action(analyzeCommand);
|
|
36
37
|
|
|
37
38
|
program
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seomd-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "The official CLI for the SEO.md open standard ā AEO infrastructure for technical founders",
|
|
5
5
|
"homepage": "https://seomd.dev",
|
|
6
6
|
"repository": {
|
|
@@ -47,4 +47,4 @@
|
|
|
47
47
|
"citation-tracking",
|
|
48
48
|
"llm-seo"
|
|
49
49
|
]
|
|
50
|
-
}
|
|
50
|
+
}
|
package/src/commands/analyze.js
CHANGED
|
@@ -9,6 +9,23 @@ import { writeAnalysisToSeoMd, writeReverseMd, writePageAnalysis } from '../util
|
|
|
9
9
|
|
|
10
10
|
dotenv.config();
|
|
11
11
|
|
|
12
|
+
function matchRoute(pattern, url) {
|
|
13
|
+
const cleanPattern = pattern.replace(/\/$/, '');
|
|
14
|
+
const cleanUrl = url.replace(/\/$/, '');
|
|
15
|
+
|
|
16
|
+
if (cleanPattern.toLowerCase() === cleanUrl.toLowerCase()) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Replace "/[param]" with optional group "(?:/([^/]+))?"
|
|
21
|
+
const regexPattern = cleanPattern
|
|
22
|
+
.replace(/\/\[[^\]]+\]/g, '(?:\\/([^/]+))?')
|
|
23
|
+
.replace(/\//g, '\\/');
|
|
24
|
+
|
|
25
|
+
const regex = new RegExp('^' + regexPattern + '\\/?$', 'i');
|
|
26
|
+
return regex.test(url);
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
export async function analyzeCommand(options) {
|
|
13
30
|
const apiKey = process.env.SEOMD_API_KEY;
|
|
14
31
|
const paymentToken = process.env.SEOMD_PAYMENT_TOKEN;
|
|
@@ -66,20 +83,35 @@ export async function analyzeCommand(options) {
|
|
|
66
83
|
}
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
//
|
|
86
|
+
// Filter by options.page if specified
|
|
87
|
+
if (options.page) {
|
|
88
|
+
pagesList = pagesList.filter(p => matchRoute(p.url, options.page));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Default fallback if no page matches or list is empty
|
|
70
92
|
if (pagesList.length === 0) {
|
|
93
|
+
let fallbackId = 'homepage';
|
|
94
|
+
if (options.page && options.page !== '/') {
|
|
95
|
+
fallbackId = options.page
|
|
96
|
+
.replace(/^\//, '')
|
|
97
|
+
.replace(/\/$/, '')
|
|
98
|
+
.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
99
|
+
}
|
|
71
100
|
pagesList.push({
|
|
72
|
-
id:
|
|
73
|
-
url: '/',
|
|
74
|
-
primary_keyword: `best ${niche}`,
|
|
101
|
+
id: fallbackId,
|
|
102
|
+
url: options.page || '/',
|
|
103
|
+
primary_keyword: data.keywords?.primary || `best ${niche}`,
|
|
75
104
|
status: 'planned'
|
|
76
105
|
});
|
|
77
106
|
}
|
|
78
107
|
|
|
79
108
|
// Extract engines
|
|
80
|
-
|
|
109
|
+
let engines = data.aeo?._analysis?.engines_tracked || ['ChatGPT'];
|
|
110
|
+
if (options.engines) {
|
|
111
|
+
engines = options.engines.split(',').map(e => e.trim());
|
|
112
|
+
}
|
|
81
113
|
|
|
82
|
-
console.log(chalk.bold.cyan(`\nš
|
|
114
|
+
console.log(chalk.bold.cyan(`\nš Foxcite: Running AI Search Audit for ${chalk.white(domain)}`));
|
|
83
115
|
console.log(chalk.dim(`Engines: ${engines.join(', ')}`));
|
|
84
116
|
console.log(chalk.dim(`Pages to scan: ${pagesList.length}`));
|
|
85
117
|
console.log('');
|
|
@@ -87,15 +119,17 @@ export async function analyzeCommand(options) {
|
|
|
87
119
|
const spinner = ora('Initializing scan sessions...').start();
|
|
88
120
|
|
|
89
121
|
try {
|
|
122
|
+
const brand = data.identity?.brand || 'My Brand';
|
|
90
123
|
const payload = {
|
|
91
124
|
domain,
|
|
92
125
|
niche,
|
|
126
|
+
brand,
|
|
93
127
|
queries,
|
|
94
128
|
engines,
|
|
95
129
|
pages: pagesList.map(p => ({
|
|
96
130
|
id: p.id,
|
|
97
|
-
url: p.url,
|
|
98
|
-
primary_keyword: p.primary_keyword
|
|
131
|
+
url: options.page || p.url, // Use the specific page requested if provided
|
|
132
|
+
primary_keyword: p.primary_keyword || data.keywords?.primary || `best ${niche}`,
|
|
99
133
|
status: p.status
|
|
100
134
|
}))
|
|
101
135
|
};
|
|
@@ -111,7 +145,8 @@ export async function analyzeCommand(options) {
|
|
|
111
145
|
await writeAnalysisToSeoMd(doc, results, cwd);
|
|
112
146
|
|
|
113
147
|
// Writeback to SEO.REVERSE.md
|
|
114
|
-
|
|
148
|
+
const brandName = data.identity?.brand || 'My Brand';
|
|
149
|
+
await writeReverseMd(cwd, results, domain, brandName);
|
|
115
150
|
|
|
116
151
|
// Writeback to .seomd/pages/*.md
|
|
117
152
|
await writePageAnalysis(cwd, results);
|
package/src/commands/sync.js
CHANGED
|
@@ -88,7 +88,8 @@ export async function syncCommand(options) {
|
|
|
88
88
|
await writeAnalysisToSeoMd(doc, results, cwd);
|
|
89
89
|
|
|
90
90
|
// Writeback to SEO.REVERSE.md
|
|
91
|
-
|
|
91
|
+
const brandName = data.identity?.brand || 'My Brand';
|
|
92
|
+
await writeReverseMd(cwd, results, domain, brandName);
|
|
92
93
|
|
|
93
94
|
// Writeback to .seomd/pages/*.md
|
|
94
95
|
await writePageAnalysis(cwd, results);
|
package/src/utils/api-client.js
CHANGED
|
@@ -9,6 +9,16 @@ const API_KEY = process.env.SEOMD_API_KEY;
|
|
|
9
9
|
const PAYMENT_TOKEN = process.env.SEOMD_PAYMENT_TOKEN;
|
|
10
10
|
const SEOMD_DOMAIN = process.env.SEOMD_DOMAIN;
|
|
11
11
|
|
|
12
|
+
const getDashboardUrlFromApi = (apiUrl) => {
|
|
13
|
+
if (apiUrl.includes('127.0.0.1') || apiUrl.includes('localhost')) {
|
|
14
|
+
return 'http://localhost:3000';
|
|
15
|
+
}
|
|
16
|
+
if (apiUrl.includes('api.foxcite.com')) {
|
|
17
|
+
return 'https://app.foxcite.com';
|
|
18
|
+
}
|
|
19
|
+
return 'https://seomd.dev';
|
|
20
|
+
};
|
|
21
|
+
|
|
12
22
|
export const client = axios.create({
|
|
13
23
|
baseURL: API_URL,
|
|
14
24
|
timeout: 300000, // 5 minutes timeout for LLM audits
|
|
@@ -32,7 +42,14 @@ client.interceptors.response.use(
|
|
|
32
42
|
return Promise.reject(new Error('Authentication failed: Invalid or missing API key (SEOMD_API_KEY).'));
|
|
33
43
|
}
|
|
34
44
|
if (status === 402) {
|
|
35
|
-
|
|
45
|
+
const dashUrl = getDashboardUrlFromApi(API_URL);
|
|
46
|
+
return Promise.reject(new Error(
|
|
47
|
+
`Insufficient scan credits.\n\n` +
|
|
48
|
+
`š For Humans: Recharge credits or upgrade your plan in the dashboard:\n` +
|
|
49
|
+
` ${dashUrl}/billing\n\n` +
|
|
50
|
+
`š¤ For Programmatic Agents:\n` +
|
|
51
|
+
` Set SEOMD_PAYMENT_TOKEN in your environment or headers to pay-per-scan inline using USDC via the agent-native x402 protocol.`
|
|
52
|
+
));
|
|
36
53
|
}
|
|
37
54
|
return Promise.reject(new Error(`API Error (${status}): ${detail}`));
|
|
38
55
|
}
|
package/src/utils/writeback.js
CHANGED
|
@@ -67,7 +67,7 @@ export async function writeAnalysisToSeoMd(doc, response, cwd) {
|
|
|
67
67
|
* @param {string} cwd - Current working directory
|
|
68
68
|
* @param {any} response - The API response from analyze/sync
|
|
69
69
|
*/
|
|
70
|
-
export async function writeReverseMd(cwd, response) {
|
|
70
|
+
export async function writeReverseMd(cwd, response, defaultDomain = 'example.com', defaultBrand = 'My Brand') {
|
|
71
71
|
const reversePath = path.join(cwd, 'SEO.REVERSE.md');
|
|
72
72
|
|
|
73
73
|
const reversePages = response.page_analysis.map(p => ({
|
|
@@ -88,8 +88,8 @@ export async function writeReverseMd(cwd, response) {
|
|
|
88
88
|
const primaryCompetitor = response.intent_analysis?.comparison?.top_cited_competitor || 'None';
|
|
89
89
|
|
|
90
90
|
const reverseDoc = {
|
|
91
|
-
domain: response.domain ||
|
|
92
|
-
brand: response.brand_name ||
|
|
91
|
+
domain: response.domain || defaultDomain,
|
|
92
|
+
brand: response.brand_name || defaultBrand,
|
|
93
93
|
primary_competitor: primaryCompetitor,
|
|
94
94
|
last_analyzed: response.aeo_analysis.last_analyzed,
|
|
95
95
|
next_analysis: response.aeo_analysis.next_analysis,
|