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 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.0.2",
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
+ }
@@ -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
- // Default to homepage if no pages defined
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: 'homepage',
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
- const engines = data.aeo?._analysis?.engines_tracked || ['Claude', 'ChatGPT'];
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šŸ“Š foxcite: Running AI Search Audit for ${chalk.white(domain)}`));
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
- await writeReverseMd(cwd, results);
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);
@@ -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
- await writeReverseMd(cwd, results);
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);
@@ -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
- return Promise.reject(new Error(`Payment Required: Insufficient scan credits. ${detail}`));
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
  }
@@ -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 || 'example.com',
92
- brand: response.brand_name || 'My Brand',
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,