@rankcli/cli 0.0.24 → 0.0.26

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.
Files changed (3) hide show
  1. package/dist/index.js +1189 -993
  2. package/dist/index.mjs +1147 -949
  3. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -8,264 +8,17 @@ import {
8
8
  import { Command } from "commander";
9
9
 
10
10
  // src/commands/init.ts
11
- import { existsSync, writeFileSync, readFileSync } from "fs";
12
- import { join } from "path";
13
- var FRAMEWORKS = [
14
- { pattern: "next.config", name: "Next.js" },
15
- { pattern: "astro.config", name: "Astro" },
16
- { pattern: "nuxt.config", name: "Nuxt" },
17
- { pattern: "svelte.config", name: "SvelteKit" },
18
- { pattern: "gatsby-config", name: "Gatsby" },
19
- { pattern: "vite.config", name: "Vite" },
20
- { pattern: "hugo.toml", name: "Hugo" },
21
- { pattern: "_config.yml", name: "Jekyll" },
22
- { pattern: "angular.json", name: "Angular" },
23
- { pattern: "vue.config", name: "Vue CLI" }
24
- ];
25
- function detectFramework() {
26
- const cwd = process.cwd();
27
- for (const fw of FRAMEWORKS) {
28
- const patterns = [
29
- `${fw.pattern}.js`,
30
- `${fw.pattern}.ts`,
31
- `${fw.pattern}.mjs`,
32
- `${fw.pattern}.cjs`,
33
- fw.pattern
34
- ];
35
- for (const p of patterns) {
36
- if (existsSync(join(cwd, p))) {
37
- return fw.name;
38
- }
39
- }
40
- }
41
- const pkgPath = join(cwd, "package.json");
42
- if (existsSync(pkgPath)) {
43
- try {
44
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
45
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
46
- if (deps["next"]) return "Next.js";
47
- if (deps["astro"]) return "Astro";
48
- if (deps["nuxt"]) return "Nuxt";
49
- if (deps["@sveltejs/kit"]) return "SvelteKit";
50
- if (deps["gatsby"]) return "Gatsby";
51
- if (deps["react"]) return "React";
52
- if (deps["vue"]) return "Vue";
53
- } catch {
54
- }
55
- }
56
- return null;
57
- }
58
- function getProjectName() {
59
- const cwd = process.cwd();
60
- const pkgPath = join(cwd, "package.json");
61
- if (existsSync(pkgPath)) {
62
- try {
63
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
64
- if (pkg.name) return pkg.name;
65
- } catch {
66
- }
67
- }
68
- return cwd.split("/").pop() || "unknown";
69
- }
70
- async function init(options) {
71
- console.log("\n\u{1F680} Initializing SEO Autopilot...\n");
72
- const configPath = join(process.cwd(), "seo-autopilot.config.json");
73
- if (existsSync(configPath)) {
74
- console.log("\u26A0\uFE0F Configuration file already exists: seo-autopilot.config.json");
75
- console.log(' Run "seo analyze" to analyze your project.\n');
76
- return;
77
- }
78
- const framework = detectFramework();
79
- const projectName = getProjectName();
80
- console.log(`\u{1F4E6} Project: ${projectName}`);
81
- console.log(`\u{1F527} Framework: ${framework || "Unknown (static site?)"}`);
82
- const config2 = {
83
- name: projectName,
84
- framework,
85
- url: null,
86
- initialized: (/* @__PURE__ */ new Date()).toISOString()
87
- };
88
- writeFileSync(configPath, JSON.stringify(config2, null, 2));
89
- console.log("\n\u2705 Created seo-autopilot.config.json");
90
- console.log("\n\u{1F4CB} Next steps:");
91
- console.log(' 1. Run "seo analyze" to analyze your project');
92
- console.log(' 2. Run "seo audit --url <your-site>" to run a technical audit');
93
- console.log(" 3. Connect Google Search Console at https://seo-autopilot.dev/connect\n");
94
- }
95
-
96
- // src/commands/analyze.ts
97
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
98
- import { join as join2, extname } from "path";
99
- var CONTENT_EXTENSIONS = [".md", ".mdx", ".html", ".tsx", ".jsx", ".vue", ".astro", ".svelte"];
100
- function findFiles(dir, extensions, maxDepth = 3, depth = 0) {
101
- if (depth > maxDepth) return [];
102
- const files = [];
103
- try {
104
- const items = readdirSync(dir);
105
- for (const item of items) {
106
- if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") {
107
- continue;
108
- }
109
- const fullPath = join2(dir, item);
110
- const stat = statSync(fullPath);
111
- if (stat.isDirectory()) {
112
- files.push(...findFiles(fullPath, extensions, maxDepth, depth + 1));
113
- } else if (extensions.includes(extname(item))) {
114
- files.push(fullPath);
115
- }
116
- }
117
- } catch {
118
- }
119
- return files;
120
- }
121
- function extractKeywords(text) {
122
- const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
123
- const frequency = {};
124
- for (const word of words) {
125
- frequency[word] = (frequency[word] || 0) + 1;
126
- }
127
- const stopWords = /* @__PURE__ */ new Set([
128
- "this",
129
- "that",
130
- "with",
131
- "from",
132
- "have",
133
- "been",
134
- "were",
135
- "will",
136
- "would",
137
- "could",
138
- "should",
139
- "their",
140
- "there",
141
- "about",
142
- "which",
143
- "when",
144
- "what",
145
- "your",
146
- "more",
147
- "some",
148
- "just",
149
- "into",
150
- "over",
151
- "such",
152
- "only",
153
- "than",
154
- "them",
155
- "then",
156
- "these",
157
- "most",
158
- "other",
159
- "also",
160
- "being",
161
- "after",
162
- "const",
163
- "function",
164
- "return",
165
- "import",
166
- "export",
167
- "default",
168
- "true",
169
- "false"
170
- ]);
171
- return Object.entries(frequency).filter(([word]) => !stopWords.has(word)).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([word]) => word);
172
- }
173
- function loadConfig() {
174
- const configPath = join2(process.cwd(), "seo-autopilot.config.json");
175
- if (existsSync2(configPath)) {
176
- try {
177
- return JSON.parse(readFileSync2(configPath, "utf-8"));
178
- } catch {
179
- return null;
180
- }
181
- }
182
- return null;
183
- }
184
- async function analyze(options) {
185
- console.log("\n\u{1F50D} Analyzing project for SEO opportunities...\n");
186
- const cwd = process.cwd();
187
- const config2 = loadConfig();
188
- const result = {
189
- projectName: config2?.name || cwd.split("/").pop() || "unknown",
190
- framework: config2?.framework || null,
191
- hasReadme: false,
192
- readmeSummary: null,
193
- pageCount: 0,
194
- hasMetaTags: false,
195
- hasSitemap: false,
196
- hasRobotsTxt: false,
197
- keywords: [],
198
- suggestions: []
199
- };
200
- const readmePaths = ["README.md", "readme.md", "Readme.md", "README.MD"];
201
- for (const rp of readmePaths) {
202
- if (existsSync2(join2(cwd, rp))) {
203
- result.hasReadme = true;
204
- const content = readFileSync2(join2(cwd, rp), "utf-8");
205
- result.readmeSummary = content.slice(0, 200).replace(/\n/g, " ").trim() + "...";
206
- result.keywords.push(...extractKeywords(content));
207
- break;
208
- }
209
- }
210
- result.hasSitemap = existsSync2(join2(cwd, "public", "sitemap.xml")) || existsSync2(join2(cwd, "sitemap.xml"));
211
- result.hasRobotsTxt = existsSync2(join2(cwd, "public", "robots.txt")) || existsSync2(join2(cwd, "robots.txt"));
212
- const contentFiles = findFiles(cwd, CONTENT_EXTENSIONS);
213
- result.pageCount = contentFiles.length;
214
- const htmlFiles = contentFiles.filter((f) => f.endsWith(".html") || f.includes("_document") || f.includes("layout"));
215
- for (const hf of htmlFiles.slice(0, 5)) {
216
- const content = readFileSync2(hf, "utf-8");
217
- if (content.includes("<meta") || content.includes("metadata")) {
218
- result.hasMetaTags = true;
219
- break;
220
- }
221
- }
222
- for (const cf of contentFiles.slice(0, 10)) {
223
- const content = readFileSync2(cf, "utf-8");
224
- result.keywords.push(...extractKeywords(content));
225
- }
226
- result.keywords = [...new Set(result.keywords)].slice(0, 20);
227
- if (!result.hasReadme) {
228
- result.suggestions.push("Add a README.md with clear project description");
229
- }
230
- if (!result.hasSitemap) {
231
- result.suggestions.push("Generate a sitemap.xml for better crawlability");
232
- }
233
- if (!result.hasRobotsTxt) {
234
- result.suggestions.push("Add robots.txt to guide search engine crawlers");
235
- }
236
- if (!result.hasMetaTags) {
237
- result.suggestions.push("Add meta tags (title, description, og:*) to pages");
238
- }
239
- if (result.pageCount < 5) {
240
- result.suggestions.push("Consider adding more content pages for SEO coverage");
241
- }
242
- console.log("\u{1F4CA} Analysis Results\n");
243
- console.log(`Project: ${result.projectName}`);
244
- console.log(`Framework: ${result.framework || "Unknown"}`);
245
- console.log(`Content files: ${result.pageCount}`);
246
- console.log(`Has README: ${result.hasReadme ? "\u2705" : "\u274C"}`);
247
- console.log(`Has sitemap.xml: ${result.hasSitemap ? "\u2705" : "\u274C"}`);
248
- console.log(`Has robots.txt: ${result.hasRobotsTxt ? "\u2705" : "\u274C"}`);
249
- console.log(`Has meta tags: ${result.hasMetaTags ? "\u2705" : "\u274C"}`);
250
- if (result.keywords.length > 0) {
251
- console.log("\n\u{1F511} Detected Keywords:");
252
- console.log(` ${result.keywords.join(", ")}`);
253
- }
254
- if (result.suggestions.length > 0) {
255
- console.log("\n\u{1F4A1} Suggestions:");
256
- for (const s of result.suggestions) {
257
- console.log(` \u2022 ${s}`);
258
- }
259
- } else {
260
- console.log("\n\u2705 No critical SEO issues found!");
261
- }
262
- console.log('\n\u{1F4C8} Next: Run "seo audit --url <your-deployed-site>" for a full technical audit\n');
263
- }
11
+ import { existsSync as existsSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
12
+ import { join as join2 } from "path";
13
+ import { execSync } from "child_process";
14
+ import chalk2 from "chalk";
15
+ import inquirer2 from "inquirer";
264
16
 
265
- // src/commands/audit.ts
266
- import { runFullAudit, discoverPagesFromLinks, frameworks } from "@rankcli/agent-runtime";
17
+ // src/commands/auth.ts
18
+ import chalk from "chalk";
267
19
  import ora from "ora";
268
- import chalk2 from "chalk";
20
+ import inquirer from "inquirer";
21
+ import open from "open";
269
22
 
270
23
  // src/lib/config.ts
271
24
  import Conf from "conf";
@@ -354,11 +107,14 @@ function isApiKeyAuth() {
354
107
  function getApiKey() {
355
108
  return process.env.RANKCLI_API_KEY || config.get("apiKey") || null;
356
109
  }
357
- function saveApiKey(apiKey, keyName) {
110
+ function saveApiKey(apiKey, keyName, email) {
358
111
  config.set("apiKey", apiKey);
359
112
  if (keyName) {
360
113
  config.set("apiKeyName", keyName);
361
114
  }
115
+ if (email) {
116
+ config.set("email", email);
117
+ }
362
118
  }
363
119
  async function validateApiKey(apiKey) {
364
120
  try {
@@ -464,34 +220,832 @@ async function getAuthenticatedClient() {
464
220
  return supabase;
465
221
  }
466
222
 
467
- // src/lib/upsell.ts
468
- import chalk from "chalk";
469
- var WEB_APP_URL2 = "https://rankcli.dev";
470
- var TIER_LIMITS = {
471
- free_anon: {
472
- checks: 50,
473
- sites: 0,
474
- domains_per_month: 1,
475
- features: { geo: false, auto_fix: false, github: false, ai: false }
476
- },
477
- free: {
478
- checks: 100,
479
- sites: 1,
480
- domains_per_month: 2,
481
- features: { geo: "basic", auto_fix: false, github: false, ai: false }
482
- },
483
- solo: {
484
- checks: 280,
485
- sites: 3,
486
- domains_per_month: 5,
487
- features: { geo: true, auto_fix: true, github: false, ai: true }
488
- },
489
- pro: {
490
- checks: 280,
491
- sites: 10,
492
- domains_per_month: 15,
493
- features: { geo: true, auto_fix: true, github: true, ai: true }
494
- },
223
+ // src/commands/auth.ts
224
+ async function browserLogin(spinner) {
225
+ spinner.start("Starting login...");
226
+ let start;
227
+ try {
228
+ const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-start`, { method: "POST" });
229
+ if (!res.ok) throw new Error(`status ${res.status}`);
230
+ start = await res.json();
231
+ } catch (err) {
232
+ spinner.fail("Could not start login");
233
+ console.log(chalk.red(err instanceof Error ? err.message : "Network error"));
234
+ return;
235
+ }
236
+ spinner.text = "Opening browser to approve...";
237
+ try {
238
+ await open(start.url);
239
+ } catch {
240
+ }
241
+ console.log(chalk.dim(`
242
+ If a browser didn't open, visit:
243
+ ${start.url}
244
+ `));
245
+ const deadline = Date.now() + start.expiresInSeconds * 1e3;
246
+ spinner.text = "Waiting for approval in your browser...";
247
+ while (Date.now() < deadline) {
248
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
249
+ let status;
250
+ try {
251
+ const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-status`, {
252
+ method: "POST",
253
+ headers: { "Content-Type": "application/json" },
254
+ body: JSON.stringify({ connectId: start.connectId })
255
+ });
256
+ status = await res.json();
257
+ } catch {
258
+ continue;
259
+ }
260
+ if (status.status === "claimed" && status.apiKey) {
261
+ saveApiKey(status.apiKey, status.apiKeyName, status.email);
262
+ await fetchAndSaveSubscription();
263
+ spinner.succeed(`Logged in as ${chalk.green(status.email)}`);
264
+ return;
265
+ }
266
+ if (status.status === "expired" || status.status === "not_found") {
267
+ spinner.fail("Login link expired");
268
+ console.log(chalk.dim('Run "rankcli login" again.'));
269
+ return;
270
+ }
271
+ }
272
+ spinner.fail("Timed out waiting for approval");
273
+ console.log(chalk.dim(`Run "rankcli login" again when you're ready.`));
274
+ }
275
+ async function login(options) {
276
+ if (isLoggedIn()) {
277
+ const { email } = getUserInfo();
278
+ console.log(chalk.yellow(`Already logged in as ${email}`));
279
+ console.log(chalk.dim('Run "rankcli logout" to sign out first.'));
280
+ return;
281
+ }
282
+ const spinner = ora();
283
+ try {
284
+ if (options.token) {
285
+ const apiKey = options.token;
286
+ if (!apiKey.startsWith("rankcli_")) {
287
+ console.log(chalk.red('Invalid API key format. Keys should start with "rankcli_"'));
288
+ return;
289
+ }
290
+ spinner.start("Validating API key...");
291
+ const result = await validateApiKey(apiKey);
292
+ if (!result.valid) {
293
+ spinner.fail("Invalid API key");
294
+ console.log(chalk.red(`
295
+ ${result.error}`));
296
+ return;
297
+ }
298
+ saveApiKey(apiKey);
299
+ if (result.subscription) {
300
+ saveSubscription({
301
+ planId: result.subscription.planId,
302
+ planName: result.subscription.planName,
303
+ sitesLimit: result.subscription.sitesLimit
304
+ });
305
+ }
306
+ spinner.succeed(`Authenticated via API key`);
307
+ console.log(chalk.dim(`
308
+ Email: ${result.user?.email}`));
309
+ console.log(chalk.dim(`Plan: ${result.subscription?.planName || "Free"}`));
310
+ return;
311
+ }
312
+ if (options.browser !== false && !options.email) {
313
+ await browserLogin(spinner);
314
+ } else {
315
+ let email = options.email;
316
+ if (!email) {
317
+ const answers = await inquirer.prompt([
318
+ {
319
+ type: "input",
320
+ name: "email",
321
+ message: "Email:",
322
+ validate: (input) => {
323
+ if (!input.includes("@")) return "Please enter a valid email";
324
+ return true;
325
+ }
326
+ }
327
+ ]);
328
+ email = answers.email;
329
+ }
330
+ const { password } = await inquirer.prompt([
331
+ {
332
+ type: "password",
333
+ name: "password",
334
+ message: "Password:",
335
+ mask: "*"
336
+ }
337
+ ]);
338
+ spinner.start("Signing in...");
339
+ const supabase = getSupabaseClient();
340
+ const { data, error } = await supabase.auth.signInWithPassword({
341
+ email,
342
+ password
343
+ });
344
+ if (error) {
345
+ spinner.fail("Login failed");
346
+ console.log(chalk.red(`
347
+ ${error.message}`));
348
+ if (error.message.includes("Invalid login")) {
349
+ console.log(chalk.dim("\nDon't have an account? Sign up at:"));
350
+ console.log(chalk.cyan(` ${WEB_APP_URL}/signup`));
351
+ }
352
+ return;
353
+ }
354
+ if (!data.session) {
355
+ spinner.fail("No session returned");
356
+ return;
357
+ }
358
+ saveSession(data.session);
359
+ await fetchAndSaveSubscription();
360
+ spinner.succeed(`Logged in as ${chalk.green(data.user?.email)}`);
361
+ const { planName, sitesLimit } = getUserInfo();
362
+ console.log(chalk.dim(`
363
+ Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
364
+ }
365
+ } catch (err) {
366
+ spinner.fail("Login failed");
367
+ console.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
368
+ }
369
+ }
370
+ async function logout() {
371
+ if (!isLoggedIn()) {
372
+ console.log(chalk.yellow("Not logged in"));
373
+ return;
374
+ }
375
+ const { email } = getUserInfo();
376
+ const spinner = ora("Signing out...").start();
377
+ try {
378
+ const supabase = getSupabaseClient();
379
+ await supabase.auth.signOut();
380
+ clearCredentials();
381
+ spinner.succeed(`Logged out from ${chalk.dim(email)}`);
382
+ } catch (err) {
383
+ spinner.fail("Logout failed");
384
+ console.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
385
+ }
386
+ }
387
+ async function whoami() {
388
+ if (!isLoggedIn()) {
389
+ console.log(chalk.yellow("Not logged in"));
390
+ console.log(chalk.dim('\nRun "rankcli login" to sign in.'));
391
+ console.log(chalk.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
392
+ return;
393
+ }
394
+ const spinner = ora("Checking account...").start();
395
+ try {
396
+ if (isApiKeyAuth()) {
397
+ const apiKey = getApiKey();
398
+ if (apiKey) {
399
+ const result = await validateApiKey(apiKey);
400
+ if (result.valid && result.subscription) {
401
+ saveSubscription({
402
+ planId: result.subscription.planId,
403
+ planName: result.subscription.planName,
404
+ sitesLimit: result.subscription.sitesLimit
405
+ });
406
+ }
407
+ }
408
+ } else {
409
+ await fetchAndSaveSubscription();
410
+ }
411
+ const { email, planName, sitesLimit } = getUserInfo();
412
+ const authMethod = isApiKeyAuth() ? "API Key" : "Session";
413
+ spinner.stop();
414
+ console.log(chalk.bold("\nRankCLI Account"));
415
+ console.log("\u2500".repeat(40));
416
+ console.log(` Email: ${chalk.cyan(email || "N/A")}`);
417
+ console.log(` Plan: ${chalk.green(planName)}`);
418
+ console.log(` Sites: ${sitesLimit} allowed`);
419
+ console.log(` Auth: ${chalk.dim(authMethod)}`);
420
+ console.log("\u2500".repeat(40));
421
+ console.log(chalk.dim(`
422
+ Manage at: ${WEB_APP_URL}/account`));
423
+ } catch (err) {
424
+ spinner.fail("Failed to fetch account info");
425
+ console.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
426
+ }
427
+ }
428
+ async function fetchAndSaveSubscription() {
429
+ const supabase = await getAuthenticatedClient();
430
+ if (!supabase) return;
431
+ const { userId } = getUserInfo();
432
+ if (!userId) return;
433
+ const { data: subscription } = await supabase.from("user_subscriptions").select(`
434
+ plan:subscription_plans (
435
+ plan_id,
436
+ name,
437
+ sites_limit
438
+ )
439
+ `).eq("user_id", userId).single();
440
+ if (subscription?.plan) {
441
+ const plan = subscription.plan;
442
+ saveSubscription({
443
+ planId: plan.plan_id,
444
+ planName: plan.name,
445
+ sitesLimit: plan.sites_limit
446
+ });
447
+ } else {
448
+ saveSubscription({
449
+ planId: void 0,
450
+ planName: "Free",
451
+ sitesLimit: 1
452
+ });
453
+ }
454
+ }
455
+
456
+ // src/commands/setup.ts
457
+ import * as readline from "readline";
458
+ import * as fs from "fs";
459
+ import * as path from "path";
460
+ import {
461
+ injectGA4,
462
+ injectGSCVerification,
463
+ writeGitHubActionFiles,
464
+ getGSCSetupInstructions,
465
+ generateGA4EnvTemplate
466
+ } from "@rankcli/agent-runtime";
467
+ async function setup(options) {
468
+ console.log("\n\u{1F527} RankCLI - Setup\n");
469
+ const projectPath = process.cwd();
470
+ if (!fs.existsSync(path.join(projectPath, "package.json")) && !fs.existsSync(path.join(projectPath, "index.html"))) {
471
+ console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
472
+ }
473
+ if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
474
+ await interactiveSetup(projectPath);
475
+ return;
476
+ }
477
+ if (options.ga4) {
478
+ await setupGA4(projectPath, options.ga4);
479
+ }
480
+ if (options.gsc) {
481
+ await setupGSC(projectPath, options.gsc);
482
+ }
483
+ if (options.githubAction) {
484
+ await setupGitHubAction(projectPath, options.schedule || "weekly");
485
+ }
486
+ }
487
+ async function interactiveSetup(projectPath) {
488
+ const rl = readline.createInterface({
489
+ input: process.stdin,
490
+ output: process.stdout
491
+ });
492
+ const ask2 = (question) => {
493
+ return new Promise((resolve) => {
494
+ rl.question(question, (answer) => {
495
+ resolve(answer.trim());
496
+ });
497
+ });
498
+ };
499
+ try {
500
+ console.log("This wizard will help you set up SEO tracking for your project.\n");
501
+ console.log("\u2500".repeat(60));
502
+ console.log("\u{1F4CA} STEP 1: Google Analytics 4\n");
503
+ console.log("GA4 tracks visitor behavior on your site.");
504
+ console.log("Get your Measurement ID from: https://analytics.google.com");
505
+ console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
506
+ const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
507
+ if (ga4Id && ga4Id.startsWith("G-")) {
508
+ await setupGA4(projectPath, ga4Id);
509
+ } else if (ga4Id) {
510
+ console.log('\u26A0\uFE0F Invalid format. GA4 IDs start with "G-". Skipping.\n');
511
+ } else {
512
+ console.log("Skipping GA4 setup.\n");
513
+ }
514
+ console.log("\u2500".repeat(60));
515
+ console.log("\u{1F50D} STEP 2: Google Search Console\n");
516
+ console.log("GSC shows how Google sees your site and what queries you rank for.");
517
+ console.log("Get your verification code from: https://search.google.com/search-console");
518
+ console.log("Go to: Settings > Ownership verification > HTML tag\n");
519
+ const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
520
+ if (gscCode && gscCode.length > 10) {
521
+ await setupGSC(projectPath, gscCode);
522
+ } else if (gscCode) {
523
+ console.log("\u26A0\uFE0F Code seems too short. Skipping.\n");
524
+ } else {
525
+ console.log("Skipping GSC verification.\n");
526
+ }
527
+ console.log("\u2500".repeat(60));
528
+ console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
529
+ console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
530
+ console.log("It creates issues with reports and can auto-fix some problems.\n");
531
+ const setupAction = await ask2("Set up GitHub Action? (y/n): ");
532
+ if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
533
+ console.log("\nHow often should the check run?");
534
+ console.log(" 1. Weekly (recommended for most sites)");
535
+ console.log(" 2. Daily (for high-traffic sites)");
536
+ console.log(" 3. Monthly (for low-traffic sites)\n");
537
+ const scheduleChoice = await ask2("Enter choice (1-3): ");
538
+ const scheduleMap = {
539
+ "1": "weekly",
540
+ "2": "daily",
541
+ "3": "monthly"
542
+ };
543
+ const schedule = scheduleMap[scheduleChoice] || "weekly";
544
+ const siteUrl = await ask2("Enter your site URL (https://example.com): ");
545
+ if (siteUrl) {
546
+ const syncChoice = await ask2("Send scheduled audits to your RankCLI dashboard? (y/n): ");
547
+ const syncToDashboard = syncChoice.toLowerCase() === "y" || syncChoice.toLowerCase() === "yes";
548
+ await setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard);
549
+ }
550
+ } else {
551
+ console.log("Skipping GitHub Action setup.\n");
552
+ }
553
+ console.log("\u2500".repeat(60));
554
+ console.log("\u2705 Setup complete!\n");
555
+ console.log("Next steps:");
556
+ console.log(" 1. Commit the changes to your repository");
557
+ console.log(" 2. If using GitHub Actions, add required secrets (see .github/SEO_SETUP.md)");
558
+ console.log(" 3. Deploy your site to apply tracking code changes");
559
+ console.log(" 4. Verify GSC ownership and GA4 data collection\n");
560
+ rl.close();
561
+ } catch (error) {
562
+ console.log(`
563
+ \u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
564
+ `);
565
+ rl.close();
566
+ }
567
+ }
568
+ async function setupGA4(projectPath, measurementId) {
569
+ console.log(`Setting up Google Analytics 4 (${measurementId})...`);
570
+ const result = await injectGA4(projectPath, { measurementId });
571
+ if (result.success) {
572
+ console.log(`\u2705 ${result.message}`);
573
+ if (result.file) {
574
+ console.log(` Modified: ${result.file}`);
575
+ }
576
+ } else {
577
+ console.log(`\u26A0\uFE0F ${result.message}`);
578
+ if (result.code) {
579
+ console.log("\nManually add this code to your HTML <head>:\n");
580
+ console.log(result.code);
581
+ }
582
+ }
583
+ const envExamplePath = path.join(projectPath, ".env.example");
584
+ const envContent = generateGA4EnvTemplate();
585
+ if (fs.existsSync(envExamplePath)) {
586
+ const existing = fs.readFileSync(envExamplePath, "utf-8");
587
+ if (!existing.includes("GA4_MEASUREMENT_ID")) {
588
+ fs.appendFileSync(envExamplePath, "\n" + envContent);
589
+ console.log(" Updated: .env.example");
590
+ }
591
+ } else {
592
+ fs.writeFileSync(envExamplePath, envContent);
593
+ console.log(" Created: .env.example");
594
+ }
595
+ console.log("");
596
+ }
597
+ async function setupGSC(projectPath, verificationCode) {
598
+ console.log("Setting up Google Search Console verification...");
599
+ const result = await injectGSCVerification(projectPath, verificationCode);
600
+ if (result.success) {
601
+ console.log(`\u2705 ${result.message}`);
602
+ if (result.file) {
603
+ console.log(` Modified: ${result.file}`);
604
+ }
605
+ } else {
606
+ console.log(`\u26A0\uFE0F ${result.message}`);
607
+ }
608
+ console.log("");
609
+ }
610
+ async function setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard) {
611
+ console.log(`Setting up GitHub Action (${schedule} schedule)...`);
612
+ const config2 = {
613
+ schedule,
614
+ siteUrl: siteUrl || "https://example.com",
615
+ features: {
616
+ audit: true,
617
+ autoFix: true,
618
+ createIssues: true,
619
+ createPRs: true
620
+ },
621
+ syncToDashboard
622
+ };
623
+ const { files, instructions } = writeGitHubActionFiles(projectPath, config2);
624
+ console.log("\u2705 GitHub Action files created:");
625
+ for (const file of files) {
626
+ console.log(` - ${path.relative(projectPath, file)}`);
627
+ }
628
+ console.log(instructions);
629
+ }
630
+ function showGSCInstructions() {
631
+ console.log(getGSCSetupInstructions());
632
+ }
633
+
634
+ // src/commands/init.ts
635
+ var FRAMEWORKS = [
636
+ { pattern: "next.config", name: "Next.js" },
637
+ { pattern: "astro.config", name: "Astro" },
638
+ { pattern: "nuxt.config", name: "Nuxt" },
639
+ { pattern: "svelte.config", name: "SvelteKit" },
640
+ { pattern: "gatsby-config", name: "Gatsby" },
641
+ { pattern: "vite.config", name: "Vite" },
642
+ { pattern: "hugo.toml", name: "Hugo" },
643
+ { pattern: "_config.yml", name: "Jekyll" },
644
+ { pattern: "angular.json", name: "Angular" },
645
+ { pattern: "vue.config", name: "Vue CLI" }
646
+ ];
647
+ var AI_KEY_ENV_VARS = [
648
+ { env: "OPENAI_API_KEY", label: "OpenAI" },
649
+ { env: "ANTHROPIC_API_KEY", label: "Anthropic" },
650
+ { env: "OPENROUTER_API_KEY", label: "OpenRouter" },
651
+ { env: "GROQ_API_KEY", label: "Groq" },
652
+ { env: "GEMINI_API_KEY", label: "Gemini" },
653
+ { env: "GOOGLE_API_KEY", label: "Gemini" }
654
+ ];
655
+ function detectFramework() {
656
+ const cwd = process.cwd();
657
+ for (const fw of FRAMEWORKS) {
658
+ const patterns = [
659
+ `${fw.pattern}.js`,
660
+ `${fw.pattern}.ts`,
661
+ `${fw.pattern}.mjs`,
662
+ `${fw.pattern}.cjs`,
663
+ fw.pattern
664
+ ];
665
+ for (const p of patterns) {
666
+ if (existsSync2(join2(cwd, p))) {
667
+ return fw.name;
668
+ }
669
+ }
670
+ }
671
+ const pkgPath = join2(cwd, "package.json");
672
+ if (existsSync2(pkgPath)) {
673
+ try {
674
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
675
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
676
+ if (deps["next"]) return "Next.js";
677
+ if (deps["astro"]) return "Astro";
678
+ if (deps["nuxt"]) return "Nuxt";
679
+ if (deps["@sveltejs/kit"]) return "SvelteKit";
680
+ if (deps["gatsby"]) return "Gatsby";
681
+ if (deps["react"]) return "React";
682
+ if (deps["vue"]) return "Vue";
683
+ } catch {
684
+ }
685
+ }
686
+ return null;
687
+ }
688
+ function getProjectName() {
689
+ const cwd = process.cwd();
690
+ const pkgPath = join2(cwd, "package.json");
691
+ if (existsSync2(pkgPath)) {
692
+ try {
693
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
694
+ if (pkg.name) return pkg.name;
695
+ } catch {
696
+ }
697
+ }
698
+ return cwd.split("/").pop() || "unknown";
699
+ }
700
+ function runGit(args) {
701
+ try {
702
+ return execSync(`git ${args}`, { cwd: process.cwd(), stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
703
+ } catch {
704
+ return null;
705
+ }
706
+ }
707
+ function parseGitHubRepo(remoteUrl) {
708
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/);
709
+ return match ? `${match[1]}/${match[2]}` : null;
710
+ }
711
+ function detectConnectedAiKeys() {
712
+ const found = /* @__PURE__ */ new Set();
713
+ for (const { env, label } of AI_KEY_ENV_VARS) {
714
+ if (process.env[env]) found.add(label);
715
+ }
716
+ return Array.from(found);
717
+ }
718
+ async function init(options) {
719
+ console.log(chalk2.bold("\n\u{1F680} Initializing RankCLI...\n"));
720
+ const configPath = join2(process.cwd(), "rankcli.config.json");
721
+ if (existsSync2(configPath)) {
722
+ console.log(chalk2.yellow("\u26A0\uFE0F Configuration file already exists: rankcli.config.json"));
723
+ console.log(chalk2.dim(' Run "rankcli audit" to analyze your project.\n'));
724
+ return;
725
+ }
726
+ const framework = detectFramework();
727
+ const projectName = getProjectName();
728
+ const isGitRepo = runGit("rev-parse --is-inside-work-tree") === "true";
729
+ const remoteUrl = isGitRepo ? runGit("remote get-url origin") : null;
730
+ const githubRepo = remoteUrl ? parseGitHubRepo(remoteUrl) : null;
731
+ const connectedAiKeys = detectConnectedAiKeys();
732
+ console.log(`\u{1F4E6} Project: ${chalk2.cyan(projectName)}`);
733
+ console.log(`\u{1F527} Framework: ${chalk2.cyan(framework || "Unknown (static site?)")}`);
734
+ if (githubRepo) {
735
+ console.log(`\u{1F419} GitHub repo: ${chalk2.cyan(githubRepo)}`);
736
+ } else if (isGitRepo) {
737
+ console.log(chalk2.dim("\u{1F419} Git repo detected, but no GitHub remote found"));
738
+ }
739
+ if (connectedAiKeys.length > 0) {
740
+ console.log(`\u{1F511} AI key detected: ${chalk2.cyan(connectedAiKeys.join(", "))} (will be used automatically for --ai audits)`);
741
+ }
742
+ const config2 = {
743
+ name: projectName,
744
+ framework,
745
+ url: null,
746
+ githubRepo,
747
+ initialized: (/* @__PURE__ */ new Date()).toISOString()
748
+ };
749
+ writeFileSync2(configPath, JSON.stringify(config2, null, 2));
750
+ console.log(chalk2.green("\n\u2705 Created rankcli.config.json"));
751
+ if (options.yes) {
752
+ printNextSteps(githubRepo);
753
+ return;
754
+ }
755
+ if (!isLoggedIn()) {
756
+ console.log("");
757
+ const { shouldConnect } = await inquirer2.prompt([
758
+ {
759
+ type: "confirm",
760
+ name: "shouldConnect",
761
+ message: "Connect a RankCLI account? Unlocks a dashboard, audit history, and GitHub auto-fix PRs (opens your browser).",
762
+ default: true
763
+ }
764
+ ]);
765
+ if (shouldConnect) {
766
+ await login({});
767
+ }
768
+ } else {
769
+ const { email } = getUserInfo();
770
+ console.log(chalk2.dim(`
771
+ Already connected as ${email}.`));
772
+ }
773
+ const workflowPath = join2(process.cwd(), ".github", "workflows", "seo.yml");
774
+ if (githubRepo && !existsSync2(workflowPath)) {
775
+ console.log("");
776
+ console.log(chalk2.bold("\u{1F916} Automate SEO/GEO checks in CI"));
777
+ console.log(chalk2.dim(" Privacy: by default nothing is sent to RankCLI - the audit runs"));
778
+ console.log(chalk2.dim(" entirely inside your own GitHub Actions runner. If you opt into"));
779
+ console.log(chalk2.dim(" dashboard sync below, audit results (the URL and issues found,"));
780
+ console.log(chalk2.dim(" not your source code) are sent to your RankCLI account.\n"));
781
+ const { shouldSetupAction } = await inquirer2.prompt([
782
+ {
783
+ type: "confirm",
784
+ name: "shouldSetupAction",
785
+ message: `Set up a GitHub Action for ${githubRepo}?`,
786
+ default: true
787
+ }
788
+ ]);
789
+ if (shouldSetupAction) {
790
+ const { siteUrl } = await inquirer2.prompt([
791
+ {
792
+ type: "input",
793
+ name: "siteUrl",
794
+ message: "Site URL to audit (https://example.com):",
795
+ validate: (input) => {
796
+ try {
797
+ new URL(input);
798
+ return true;
799
+ } catch {
800
+ return "Please enter a valid URL";
801
+ }
802
+ }
803
+ }
804
+ ]);
805
+ const { schedule } = await inquirer2.prompt([
806
+ {
807
+ type: "list",
808
+ name: "schedule",
809
+ message: "How often should it run?",
810
+ choices: [
811
+ { name: "Weekly (recommended)", value: "weekly" },
812
+ { name: "Daily", value: "daily" },
813
+ { name: "Monthly", value: "monthly" }
814
+ ],
815
+ default: "weekly"
816
+ }
817
+ ]);
818
+ let syncToDashboard = false;
819
+ if (isLoggedIn()) {
820
+ const answer = await inquirer2.prompt([
821
+ {
822
+ type: "confirm",
823
+ name: "syncToDashboard",
824
+ message: "Also send scheduled audits to your RankCLI dashboard?",
825
+ default: true
826
+ }
827
+ ]);
828
+ syncToDashboard = answer.syncToDashboard;
829
+ }
830
+ await setupGitHubAction(process.cwd(), schedule, siteUrl, syncToDashboard);
831
+ config2.url = siteUrl;
832
+ writeFileSync2(configPath, JSON.stringify(config2, null, 2));
833
+ }
834
+ }
835
+ printNextSteps(githubRepo);
836
+ }
837
+ function printNextSteps(githubRepo) {
838
+ console.log(chalk2.bold("\n\u{1F4CB} Next steps:"));
839
+ console.log(' 1. Run "rankcli audit --url <your-site>" for a full technical audit');
840
+ console.log(' 2. Run "rankcli apply --auto" to apply fixes locally');
841
+ if (!githubRepo) {
842
+ console.log(' 3. Push to GitHub, then re-run "rankcli init" to set up automated checks');
843
+ }
844
+ console.log("");
845
+ }
846
+
847
+ // src/commands/analyze.ts
848
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
849
+ import { join as join3, extname } from "path";
850
+ var CONTENT_EXTENSIONS = [".md", ".mdx", ".html", ".tsx", ".jsx", ".vue", ".astro", ".svelte"];
851
+ function findFiles(dir, extensions, maxDepth = 3, depth = 0) {
852
+ if (depth > maxDepth) return [];
853
+ const files = [];
854
+ try {
855
+ const items = readdirSync(dir);
856
+ for (const item of items) {
857
+ if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") {
858
+ continue;
859
+ }
860
+ const fullPath = join3(dir, item);
861
+ const stat = statSync(fullPath);
862
+ if (stat.isDirectory()) {
863
+ files.push(...findFiles(fullPath, extensions, maxDepth, depth + 1));
864
+ } else if (extensions.includes(extname(item))) {
865
+ files.push(fullPath);
866
+ }
867
+ }
868
+ } catch {
869
+ }
870
+ return files;
871
+ }
872
+ function extractKeywords(text) {
873
+ const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
874
+ const frequency = {};
875
+ for (const word of words) {
876
+ frequency[word] = (frequency[word] || 0) + 1;
877
+ }
878
+ const stopWords = /* @__PURE__ */ new Set([
879
+ "this",
880
+ "that",
881
+ "with",
882
+ "from",
883
+ "have",
884
+ "been",
885
+ "were",
886
+ "will",
887
+ "would",
888
+ "could",
889
+ "should",
890
+ "their",
891
+ "there",
892
+ "about",
893
+ "which",
894
+ "when",
895
+ "what",
896
+ "your",
897
+ "more",
898
+ "some",
899
+ "just",
900
+ "into",
901
+ "over",
902
+ "such",
903
+ "only",
904
+ "than",
905
+ "them",
906
+ "then",
907
+ "these",
908
+ "most",
909
+ "other",
910
+ "also",
911
+ "being",
912
+ "after",
913
+ "const",
914
+ "function",
915
+ "return",
916
+ "import",
917
+ "export",
918
+ "default",
919
+ "true",
920
+ "false"
921
+ ]);
922
+ return Object.entries(frequency).filter(([word]) => !stopWords.has(word)).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([word]) => word);
923
+ }
924
+ function loadConfig() {
925
+ const configPath = join3(process.cwd(), "rankcli.config.json");
926
+ if (existsSync3(configPath)) {
927
+ try {
928
+ return JSON.parse(readFileSync3(configPath, "utf-8"));
929
+ } catch {
930
+ return null;
931
+ }
932
+ }
933
+ return null;
934
+ }
935
+ async function analyze(options) {
936
+ console.log("\n\u{1F50D} Analyzing project for SEO opportunities...\n");
937
+ const cwd = process.cwd();
938
+ const config2 = loadConfig();
939
+ const result = {
940
+ projectName: config2?.name || cwd.split("/").pop() || "unknown",
941
+ framework: config2?.framework || null,
942
+ hasReadme: false,
943
+ readmeSummary: null,
944
+ pageCount: 0,
945
+ hasMetaTags: false,
946
+ hasSitemap: false,
947
+ hasRobotsTxt: false,
948
+ keywords: [],
949
+ suggestions: []
950
+ };
951
+ const readmePaths = ["README.md", "readme.md", "Readme.md", "README.MD"];
952
+ for (const rp of readmePaths) {
953
+ if (existsSync3(join3(cwd, rp))) {
954
+ result.hasReadme = true;
955
+ const content = readFileSync3(join3(cwd, rp), "utf-8");
956
+ result.readmeSummary = content.slice(0, 200).replace(/\n/g, " ").trim() + "...";
957
+ result.keywords.push(...extractKeywords(content));
958
+ break;
959
+ }
960
+ }
961
+ result.hasSitemap = existsSync3(join3(cwd, "public", "sitemap.xml")) || existsSync3(join3(cwd, "sitemap.xml"));
962
+ result.hasRobotsTxt = existsSync3(join3(cwd, "public", "robots.txt")) || existsSync3(join3(cwd, "robots.txt"));
963
+ const contentFiles = findFiles(cwd, CONTENT_EXTENSIONS);
964
+ result.pageCount = contentFiles.length;
965
+ const htmlFiles = contentFiles.filter((f) => f.endsWith(".html") || f.includes("_document") || f.includes("layout"));
966
+ for (const hf of htmlFiles.slice(0, 5)) {
967
+ const content = readFileSync3(hf, "utf-8");
968
+ if (content.includes("<meta") || content.includes("metadata")) {
969
+ result.hasMetaTags = true;
970
+ break;
971
+ }
972
+ }
973
+ for (const cf of contentFiles.slice(0, 10)) {
974
+ const content = readFileSync3(cf, "utf-8");
975
+ result.keywords.push(...extractKeywords(content));
976
+ }
977
+ result.keywords = [...new Set(result.keywords)].slice(0, 20);
978
+ if (!result.hasReadme) {
979
+ result.suggestions.push("Add a README.md with clear project description");
980
+ }
981
+ if (!result.hasSitemap) {
982
+ result.suggestions.push("Generate a sitemap.xml for better crawlability");
983
+ }
984
+ if (!result.hasRobotsTxt) {
985
+ result.suggestions.push("Add robots.txt to guide search engine crawlers");
986
+ }
987
+ if (!result.hasMetaTags) {
988
+ result.suggestions.push("Add meta tags (title, description, og:*) to pages");
989
+ }
990
+ if (result.pageCount < 5) {
991
+ result.suggestions.push("Consider adding more content pages for SEO coverage");
992
+ }
993
+ console.log("\u{1F4CA} Analysis Results\n");
994
+ console.log(`Project: ${result.projectName}`);
995
+ console.log(`Framework: ${result.framework || "Unknown"}`);
996
+ console.log(`Content files: ${result.pageCount}`);
997
+ console.log(`Has README: ${result.hasReadme ? "\u2705" : "\u274C"}`);
998
+ console.log(`Has sitemap.xml: ${result.hasSitemap ? "\u2705" : "\u274C"}`);
999
+ console.log(`Has robots.txt: ${result.hasRobotsTxt ? "\u2705" : "\u274C"}`);
1000
+ console.log(`Has meta tags: ${result.hasMetaTags ? "\u2705" : "\u274C"}`);
1001
+ if (result.keywords.length > 0) {
1002
+ console.log("\n\u{1F511} Detected Keywords:");
1003
+ console.log(` ${result.keywords.join(", ")}`);
1004
+ }
1005
+ if (result.suggestions.length > 0) {
1006
+ console.log("\n\u{1F4A1} Suggestions:");
1007
+ for (const s of result.suggestions) {
1008
+ console.log(` \u2022 ${s}`);
1009
+ }
1010
+ } else {
1011
+ console.log("\n\u2705 No critical SEO issues found!");
1012
+ }
1013
+ console.log('\n\u{1F4C8} Next: Run "rankcli audit --url <your-deployed-site>" for a full technical audit\n');
1014
+ }
1015
+
1016
+ // src/commands/audit.ts
1017
+ import { runFullAudit, discoverPagesFromLinks, frameworks } from "@rankcli/agent-runtime";
1018
+ import ora2 from "ora";
1019
+ import chalk4 from "chalk";
1020
+
1021
+ // src/lib/upsell.ts
1022
+ import chalk3 from "chalk";
1023
+ var WEB_APP_URL2 = "https://rankcli.dev";
1024
+ var TIER_LIMITS = {
1025
+ free_anon: {
1026
+ checks: 50,
1027
+ sites: 0,
1028
+ domains_per_month: 1,
1029
+ features: { geo: false, auto_fix: false, github: false, ai: false }
1030
+ },
1031
+ free: {
1032
+ checks: 100,
1033
+ sites: 1,
1034
+ domains_per_month: 2,
1035
+ features: { geo: "basic", auto_fix: false, github: false, ai: false }
1036
+ },
1037
+ solo: {
1038
+ checks: 280,
1039
+ sites: 3,
1040
+ domains_per_month: 5,
1041
+ features: { geo: true, auto_fix: true, github: false, ai: true }
1042
+ },
1043
+ pro: {
1044
+ checks: 280,
1045
+ sites: 10,
1046
+ domains_per_month: 15,
1047
+ features: { geo: true, auto_fix: true, github: true, ai: true }
1048
+ },
495
1049
  agency: {
496
1050
  checks: 280,
497
1051
  sites: 50,
@@ -504,133 +1058,133 @@ var UPSELL_MESSAGES = {
504
1058
  // After free audit completes
505
1059
  free_audit_complete: () => [
506
1060
  "",
507
- chalk.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
508
- chalk.yellow.bold(" \u{1F680} Get 2x more checks + audit history"),
509
- chalk.dim(" Create a free account to unlock 100 checks and save your results."),
1061
+ chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1062
+ chalk3.yellow.bold(" \u{1F680} Get 2x more checks + audit history"),
1063
+ chalk3.dim(" Create a free account to unlock 100 checks and save your results."),
510
1064
  "",
511
- chalk.cyan(` \u2192 rankcli login`),
512
- chalk.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1065
+ chalk3.cyan(` \u2192 rankcli login`),
1066
+ chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
513
1067
  ""
514
1068
  ],
515
1069
  // When checks limit is reached
516
1070
  checks_limit: (ctx) => [
517
1071
  "",
518
- chalk.yellow(`\u26A0\uFE0F Running ${ctx.current || 50} of ${TIER_LIMITS.solo.checks} available checks`),
519
- chalk.dim(` Unlock all ${TIER_LIMITS.solo.checks}+ checks with Solo ($9/mo)`),
520
- chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_checks`),
1072
+ chalk3.yellow(`\u26A0\uFE0F Running ${ctx.current || 50} of ${TIER_LIMITS.solo.checks} available checks`),
1073
+ chalk3.dim(` Unlock all ${TIER_LIMITS.solo.checks}+ checks with Solo ($9/mo)`),
1074
+ chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_checks`),
521
1075
  ""
522
1076
  ],
523
1077
  // When domain limit is reached
524
1078
  domain_limit: (ctx) => [
525
1079
  "",
526
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
527
- chalk.red.bold(" \u2717 Domain limit reached"),
528
- chalk.dim(` Your ${ctx.tier} plan allows ${ctx.max} domains per month.`),
1080
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1081
+ chalk3.red.bold(" \u2717 Domain limit reached"),
1082
+ chalk3.dim(` Your ${ctx.tier} plan allows ${ctx.max} domains per month.`),
529
1083
  "",
530
- chalk.dim(" Upgrade to audit more sites:"),
531
- chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_domain_limit`),
532
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1084
+ chalk3.dim(" Upgrade to audit more sites:"),
1085
+ chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_domain_limit`),
1086
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
533
1087
  ""
534
1088
  ],
535
1089
  // When GEO features are limited
536
1090
  geo_limited: (ctx) => [
537
1091
  "",
538
- chalk.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
539
- chalk.yellow.bold(" \u{1F916} Full GEO analysis requires Solo"),
540
- chalk.dim(" You're seeing basic AI crawler access checks."),
541
- chalk.dim(" Full analysis includes: LLM signals, citation readiness, content structure"),
1092
+ chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1093
+ chalk3.yellow.bold(" \u{1F916} Full GEO analysis requires Solo"),
1094
+ chalk3.dim(" You're seeing basic AI crawler access checks."),
1095
+ chalk3.dim(" Full analysis includes: LLM signals, citation readiness, content structure"),
542
1096
  "",
543
- chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_geo`),
544
- chalk.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1097
+ chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_geo`),
1098
+ chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
545
1099
  ""
546
1100
  ],
547
1101
  // When auto-fix is attempted without access
548
1102
  auto_fix_blocked: () => [
549
1103
  "",
550
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
551
- chalk.red.bold(" \u2717 Auto-fix requires Solo plan"),
552
- chalk.dim(" Apply framework-specific SEO fixes directly to your codebase."),
1104
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1105
+ chalk3.red.bold(" \u2717 Auto-fix requires Solo plan"),
1106
+ chalk3.dim(" Apply framework-specific SEO fixes directly to your codebase."),
553
1107
  "",
554
- chalk.dim(" Solo ($9/mo) includes:"),
555
- chalk.dim(" \u2022 Local auto-fix via CLI"),
556
- chalk.dim(" \u2022 25+ framework support"),
557
- chalk.dim(" \u2022 Full 280+ checks"),
1108
+ chalk3.dim(" Solo ($9/mo) includes:"),
1109
+ chalk3.dim(" \u2022 Local auto-fix via CLI"),
1110
+ chalk3.dim(" \u2022 25+ framework support"),
1111
+ chalk3.dim(" \u2022 Full 280+ checks"),
558
1112
  "",
559
- chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_autofix`),
560
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1113
+ chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_autofix`),
1114
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
561
1115
  ""
562
1116
  ],
563
1117
  // When GitHub integration is attempted without access
564
1118
  github_blocked: () => [
565
1119
  "",
566
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
567
- chalk.red.bold(" \u2717 GitHub auto-fix PRs require Pro"),
568
- chalk.dim(" Automatically create fix PRs when SEO issues are detected."),
1120
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1121
+ chalk3.red.bold(" \u2717 GitHub auto-fix PRs require Pro"),
1122
+ chalk3.dim(" Automatically create fix PRs when SEO issues are detected."),
569
1123
  "",
570
- chalk.dim(" Pro ($29/mo) includes:"),
571
- chalk.dim(" \u2022 GitHub App integration"),
572
- chalk.dim(" \u2022 Auto-fix PRs"),
573
- chalk.dim(" \u2022 Daily scheduled audits"),
574
- chalk.dim(" \u2022 Slack/Discord alerts"),
1124
+ chalk3.dim(" Pro ($29/mo) includes:"),
1125
+ chalk3.dim(" \u2022 GitHub App integration"),
1126
+ chalk3.dim(" \u2022 Auto-fix PRs"),
1127
+ chalk3.dim(" \u2022 Daily scheduled audits"),
1128
+ chalk3.dim(" \u2022 Slack/Discord alerts"),
575
1129
  "",
576
- chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_github`),
577
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1130
+ chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_github`),
1131
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
578
1132
  ""
579
1133
  ],
580
1134
  // Keyword tracking limit reached
581
1135
  keyword_limit: (ctx) => [
582
1136
  "",
583
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
584
- chalk.red.bold(" \u2717 Keyword tracking limit reached"),
585
- chalk.dim(` Your ${ctx.tier} plan allows ${ctx.limit || ctx.max} tracked keywords.`),
586
- chalk.dim(` Currently tracking: ${ctx.current}`),
1137
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1138
+ chalk3.red.bold(" \u2717 Keyword tracking limit reached"),
1139
+ chalk3.dim(` Your ${ctx.tier} plan allows ${ctx.limit || ctx.max} tracked keywords.`),
1140
+ chalk3.dim(` Currently tracking: ${ctx.current}`),
587
1141
  "",
588
- chalk.dim(" Upgrade for more keywords:"),
589
- chalk.dim(" \u2022 Solo ($9/mo): 100 keywords"),
590
- chalk.dim(" \u2022 Pro ($29/mo): 500 keywords"),
591
- chalk.dim(" \u2022 Agency ($99/mo): 2,000 keywords"),
1142
+ chalk3.dim(" Upgrade for more keywords:"),
1143
+ chalk3.dim(" \u2022 Solo ($9/mo): 100 keywords"),
1144
+ chalk3.dim(" \u2022 Pro ($29/mo): 500 keywords"),
1145
+ chalk3.dim(" \u2022 Agency ($99/mo): 2,000 keywords"),
592
1146
  "",
593
- chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_keyword_limit`),
594
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1147
+ chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_keyword_limit`),
1148
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
595
1149
  ""
596
1150
  ],
597
1151
  // Rate limit exceeded
598
1152
  rate_limit: (ctx) => [
599
1153
  "",
600
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
601
- chalk.red.bold(" \u2717 API rate limit exceeded"),
602
- chalk.dim(` Your ${ctx.tier} plan allows ${ctx.max} requests/day.`),
1154
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1155
+ chalk3.red.bold(" \u2717 API rate limit exceeded"),
1156
+ chalk3.dim(` Your ${ctx.tier} plan allows ${ctx.max} requests/day.`),
603
1157
  "",
604
- chalk.dim(" Need more? Upgrade for higher limits:"),
605
- chalk.dim(" \u2022 Solo: 100 req/day"),
606
- chalk.dim(" \u2022 Pro: 1,000 req/day"),
607
- chalk.dim(" \u2022 Agency: 10,000 req/day"),
1158
+ chalk3.dim(" Need more? Upgrade for higher limits:"),
1159
+ chalk3.dim(" \u2022 Solo: 100 req/day"),
1160
+ chalk3.dim(" \u2022 Pro: 1,000 req/day"),
1161
+ chalk3.dim(" \u2022 Agency: 10,000 req/day"),
608
1162
  "",
609
- chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_rate_limit`),
610
- chalk.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1163
+ chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_rate_limit`),
1164
+ chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
611
1165
  ""
612
1166
  ],
613
1167
  // Schedule exceeded (for GitHub Action)
614
1168
  schedule_exceeded: (ctx) => [
615
1169
  "",
616
- chalk.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
617
- chalk.yellow.bold(" \u23F0 Scheduled audit skipped"),
618
- chalk.dim(` Your ${ctx.tier} plan allows ${ctx.limit} scheduled audits.`),
1170
+ chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1171
+ chalk3.yellow.bold(" \u23F0 Scheduled audit skipped"),
1172
+ chalk3.dim(` Your ${ctx.tier} plan allows ${ctx.limit} scheduled audits.`),
619
1173
  "",
620
- chalk.dim(" Upgrade for more frequent audits:"),
621
- chalk.dim(" \u2022 Solo: Weekly"),
622
- chalk.dim(" \u2022 Pro: Daily"),
623
- chalk.dim(" \u2022 Agency: Hourly"),
1174
+ chalk3.dim(" Upgrade for more frequent audits:"),
1175
+ chalk3.dim(" \u2022 Solo: Weekly"),
1176
+ chalk3.dim(" \u2022 Pro: Daily"),
1177
+ chalk3.dim(" \u2022 Agency: Hourly"),
624
1178
  "",
625
- chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=schedule_exceeded`),
626
- chalk.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1179
+ chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=schedule_exceeded`),
1180
+ chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
627
1181
  ""
628
1182
  ],
629
1183
  // Login prompt (soft)
630
1184
  login_prompt: () => [
631
1185
  "",
632
- chalk.dim(" \u{1F4A1} Tip: Log in to sync results and track progress over time:"),
633
- chalk.dim(" rankcli login"),
1186
+ chalk3.dim(" \u{1F4A1} Tip: Log in to sync results and track progress over time:"),
1187
+ chalk3.dim(" rankcli login"),
634
1188
  ""
635
1189
  ]
636
1190
  };
@@ -671,9 +1225,16 @@ async function fetchHtml(url, timeout = 1e4) {
671
1225
  return null;
672
1226
  }
673
1227
  }
1228
+ var AI_PROVIDER_DEFAULTS = {
1229
+ openai: { baseUrl: "https://api.openai.com/v1", model: "gpt-4o-mini" },
1230
+ openrouter: { baseUrl: "https://openrouter.ai/api/v1", model: "deepseek/deepseek-r1:free" },
1231
+ groq: { baseUrl: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile" },
1232
+ gemini: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", model: "gemini-2.0-flash" }
1233
+ };
674
1234
  async function runAIAnalysis(url, issues, provider, apiKey) {
675
1235
  const isAnthropic = provider === "anthropic";
676
- const apiUrl = isAnthropic ? "https://api.anthropic.com/v1/messages" : "https://api.openai.com/v1/chat/completions";
1236
+ const providerConfig = AI_PROVIDER_DEFAULTS[provider] || AI_PROVIDER_DEFAULTS.openai;
1237
+ const apiUrl = isAnthropic ? "https://api.anthropic.com/v1/messages" : `${providerConfig.baseUrl}/chat/completions`;
677
1238
  const prompt = `Analyze these SEO audit results and provide prioritized recommendations:
678
1239
 
679
1240
  URL: ${url}
@@ -704,19 +1265,26 @@ Return JSON with:
704
1265
  });
705
1266
  } else {
706
1267
  headers["Authorization"] = `Bearer ${apiKey}`;
1268
+ if (provider === "openrouter") {
1269
+ headers["HTTP-Referer"] = "https://rankcli.dev";
1270
+ headers["X-Title"] = "RankCLI";
1271
+ }
707
1272
  body = JSON.stringify({
708
- model: "gpt-4o-mini",
1273
+ model: providerConfig.model,
709
1274
  messages: [
710
1275
  { role: "system", content: "You are an SEO expert. Respond with valid JSON only." },
711
1276
  { role: "user", content: prompt }
712
1277
  ],
713
1278
  max_tokens: 1500,
714
- response_format: { type: "json_object" }
1279
+ // json_object mode is an OpenAI-specific extension - Groq supports
1280
+ // it too, but not every OpenAI-compatible server does, so only
1281
+ // request it for providers confirmed to support it.
1282
+ ...provider === "openai" || provider === "groq" ? { response_format: { type: "json_object" } } : {}
715
1283
  });
716
1284
  }
717
1285
  const response = await fetch(apiUrl, { method: "POST", headers, body });
718
1286
  if (!response.ok) {
719
- console.error(chalk2.yellow(`AI analysis failed: ${response.status}`));
1287
+ console.error(chalk4.yellow(`AI analysis failed: ${response.status}`));
720
1288
  return null;
721
1289
  }
722
1290
  const data = await response.json();
@@ -726,7 +1294,7 @@ Return JSON with:
726
1294
  if (!jsonMatch) return null;
727
1295
  return JSON.parse(jsonMatch[0]);
728
1296
  } catch (error) {
729
- console.error(chalk2.yellow(`AI analysis error: ${error}`));
1297
+ console.error(chalk4.yellow(`AI analysis error: ${error}`));
730
1298
  return null;
731
1299
  }
732
1300
  }
@@ -761,9 +1329,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
761
1329
  const { count } = await supabase.from("projects").select("id", { count: "exact", head: true }).eq("user_id", userId).eq("is_active", true);
762
1330
  const limit = sitesLimit ?? 1;
763
1331
  if (count !== null && count >= limit) {
764
- console.log(chalk2.yellow(`
1332
+ console.log(chalk4.yellow(`
765
1333
  \u26A0\uFE0F Project limit reached (${count}/${limit})`));
766
- console.log(chalk2.dim(` Upgrade at: ${WEB_APP_URL}/account`));
1334
+ console.log(chalk4.dim(` Upgrade at: ${WEB_APP_URL}/account`));
767
1335
  return null;
768
1336
  }
769
1337
  const { data: newProject, error: projectError } = await supabase.from("projects").insert({
@@ -773,7 +1341,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
773
1341
  domain
774
1342
  }).select("id").single();
775
1343
  if (projectError || !newProject) {
776
- console.error(chalk2.dim("Failed to create project"));
1344
+ console.error(chalk4.dim("Failed to create project"));
777
1345
  return null;
778
1346
  }
779
1347
  projectId = newProject.id;
@@ -814,7 +1382,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
814
1382
  ai_analysis: aiAnalysis
815
1383
  }).select("id").single();
816
1384
  if (auditError || !audit2) {
817
- console.error(chalk2.dim("Failed to save audit"));
1385
+ console.error(chalk4.dim("Failed to save audit"));
818
1386
  return null;
819
1387
  }
820
1388
  if (mergedIssues.length > 0) {
@@ -842,7 +1410,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
842
1410
  }).eq("id", projectId);
843
1411
  return { projectId, auditId: audit2.id };
844
1412
  } catch (error) {
845
- console.error(chalk2.dim("Cloud sync failed"));
1413
+ console.error(chalk4.dim("Cloud sync failed"));
846
1414
  return null;
847
1415
  }
848
1416
  }
@@ -850,8 +1418,16 @@ async function audit(options) {
850
1418
  const url = options.url;
851
1419
  const maxPages = parseInt(options.maxPages || "5", 10);
852
1420
  const enableAI = options.ai || false;
853
- const aiProvider = options.aiProvider || "openai";
854
- const aiKey = options.aiKey || (aiProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY : process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY) || "";
1421
+ const detectedProvider = options.aiProvider || (process.env.OPENROUTER_API_KEY ? "openrouter" : process.env.GROQ_API_KEY ? "groq" : process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY ? "gemini" : process.env.ANTHROPIC_API_KEY ? "anthropic" : "openai");
1422
+ const aiProvider = detectedProvider;
1423
+ const providerEnvKey = {
1424
+ openai: process.env.OPENAI_API_KEY,
1425
+ anthropic: process.env.ANTHROPIC_API_KEY,
1426
+ openrouter: process.env.OPENROUTER_API_KEY,
1427
+ groq: process.env.GROQ_API_KEY,
1428
+ gemini: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
1429
+ };
1430
+ const aiKey = options.aiKey || providerEnvKey[aiProvider] || "";
855
1431
  const loggedIn = isLoggedIn();
856
1432
  const { planId } = getUserInfo();
857
1433
  const tierId = planId && planId in TIER_LIMITS ? planId : loggedIn ? "free" : "free_anon";
@@ -864,7 +1440,7 @@ async function audit(options) {
864
1440
  await initEngine(passphrase);
865
1441
  premiumChecks = await getPremiumChecks();
866
1442
  } catch (error) {
867
- console.error(chalk2.dim(`(premium checks unavailable: ${error instanceof Error ? error.message : error})`));
1443
+ console.error(chalk4.dim(`(premium checks unavailable: ${error instanceof Error ? error.message : error})`));
868
1444
  }
869
1445
  }
870
1446
  }
@@ -874,12 +1450,12 @@ async function audit(options) {
874
1450
  console.log("\nOptions:");
875
1451
  console.log(" --max-pages <n> Max pages to crawl (default: 5)");
876
1452
  console.log(" --ai Enable AI analysis");
877
- console.log(" --ai-provider <p> AI provider: openai or anthropic");
878
- console.log(" --ai-key <key> API key (or set OPENAI_API_KEY/ANTHROPIC_API_KEY)");
1453
+ console.log(" --ai-provider <p> AI provider: openai, anthropic, openrouter, groq, or gemini (auto-detected from env if omitted)");
1454
+ console.log(" --ai-key <key> API key (or set OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, GROQ_API_KEY, or GEMINI_API_KEY)");
879
1455
  console.log(" --output json Output as JSON");
880
1456
  if (!loggedIn) {
881
- console.log(chalk2.dim("\nTip: Log in to sync results to the web dashboard:"));
882
- console.log(chalk2.dim(" rankcli login"));
1457
+ console.log(chalk4.dim("\nTip: Log in to sync results to the web dashboard:"));
1458
+ console.log(chalk4.dim(" rankcli login"));
883
1459
  }
884
1460
  console.log("");
885
1461
  return;
@@ -892,7 +1468,7 @@ async function audit(options) {
892
1468
  }
893
1469
  const startTime = Date.now();
894
1470
  try {
895
- const discoverySpinner = ora("Discovering pages...").start();
1471
+ const discoverySpinner = ora2("Discovering pages...").start();
896
1472
  let pagesToAudit = [url];
897
1473
  const homepageHtml = await fetchHtml(url, 1e4);
898
1474
  let detectedFramework = detectFramework2({});
@@ -906,7 +1482,7 @@ async function audit(options) {
906
1482
  } else {
907
1483
  discoverySpinner.warn("Could not fetch homepage, auditing URL only");
908
1484
  }
909
- const auditSpinner = ora(`Auditing ${pagesToAudit.length} pages in parallel...`).start();
1485
+ const auditSpinner = ora2(`Auditing ${pagesToAudit.length} pages in parallel...`).start();
910
1486
  const auditPage = async (pageUrl, index) => {
911
1487
  try {
912
1488
  const report = await runFullAudit({
@@ -953,7 +1529,7 @@ async function audit(options) {
953
1529
  const mergedIssues = mergeIssues(allIssues);
954
1530
  let aiAnalysis = null;
955
1531
  if (enableAI && aiKey) {
956
- const aiSpinner = ora(`Running AI analysis (${aiProvider})...`).start();
1532
+ const aiSpinner = ora2(`Running AI analysis (${aiProvider})...`).start();
957
1533
  aiAnalysis = await runAIAnalysis(url, mergedIssues, aiProvider, aiKey);
958
1534
  if (aiAnalysis) {
959
1535
  aiSpinner.succeed("AI analysis complete");
@@ -961,8 +1537,8 @@ async function audit(options) {
961
1537
  aiSpinner.warn("AI analysis failed or returned no results");
962
1538
  }
963
1539
  } else if (enableAI && !aiKey) {
964
- console.log(chalk2.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
965
- console.log(chalk2.dim(" Set OPENAI_API_KEY or ANTHROPIC_API_KEY, or use --ai-key"));
1540
+ console.log(chalk4.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
1541
+ console.log(chalk4.dim(" Set OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, GROQ_API_KEY, or GEMINI_API_KEY, or use --ai-key"));
966
1542
  }
967
1543
  const totalTime = Date.now() - startTime;
968
1544
  const frameworkName = detectedFramework.framework !== "unknown" ? getFrameworkDisplayName(detectedFramework.framework) : null;
@@ -990,27 +1566,27 @@ async function audit(options) {
990
1566
  };
991
1567
  console.log(JSON.stringify(jsonOutput, null, 2));
992
1568
  } else {
993
- console.log("\n" + chalk2.bold("\u2550".repeat(60)));
994
- console.log(chalk2.bold.cyan(` SEO AUDIT REPORT: ${url}`));
995
- console.log(chalk2.bold("\u2550".repeat(60)));
1569
+ console.log("\n" + chalk4.bold("\u2550".repeat(60)));
1570
+ console.log(chalk4.bold.cyan(` SEO AUDIT REPORT: ${url}`));
1571
+ console.log(chalk4.bold("\u2550".repeat(60)));
996
1572
  const score = primaryReport.healthScore?.overall || 0;
997
- const scoreColor = score >= 80 ? chalk2.green : score >= 60 ? chalk2.yellow : chalk2.red;
1573
+ const scoreColor = score >= 80 ? chalk4.green : score >= 60 ? chalk4.yellow : chalk4.red;
998
1574
  console.log(`
999
- ${chalk2.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
1000
- console.log(` ${chalk2.bold("Pages audited:")} ${successCount}`);
1001
- console.log(` ${chalk2.bold("Issues found:")} ${mergedIssues.length}`);
1002
- console.log(` ${chalk2.bold("Time:")} ${totalTime}ms`);
1575
+ ${chalk4.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
1576
+ console.log(` ${chalk4.bold("Pages audited:")} ${successCount}`);
1577
+ console.log(` ${chalk4.bold("Issues found:")} ${mergedIssues.length}`);
1578
+ console.log(` ${chalk4.bold("Time:")} ${totalTime}ms`);
1003
1579
  if (frameworkName) {
1004
- console.log(` ${chalk2.bold("Framework:")} ${chalk2.cyan(frameworkName)}`);
1580
+ console.log(` ${chalk4.bold("Framework:")} ${chalk4.cyan(frameworkName)}`);
1005
1581
  }
1006
1582
  if (primaryReport.healthScore) {
1007
- console.log("\n" + chalk2.bold(" Health Scores:"));
1583
+ console.log("\n" + chalk4.bold(" Health Scores:"));
1008
1584
  const hs = primaryReport.healthScore;
1009
1585
  const bar = (score2) => {
1010
1586
  const filled = Math.round(score2 / 5);
1011
1587
  const empty = 20 - filled;
1012
- const color = score2 >= 80 ? chalk2.green : score2 >= 60 ? chalk2.yellow : chalk2.red;
1013
- return color("\u2588".repeat(filled)) + chalk2.gray("\u2591".repeat(empty));
1588
+ const color = score2 >= 80 ? chalk4.green : score2 >= 60 ? chalk4.yellow : chalk4.red;
1589
+ return color("\u2588".repeat(filled)) + chalk4.gray("\u2591".repeat(empty));
1014
1590
  };
1015
1591
  console.log(` Crawlability: ${bar(hs.crawlability || 0)} ${hs.crawlability || 0}%`);
1016
1592
  console.log(` On-Page: ${bar(hs.onPage || 0)} ${hs.onPage || 0}%`);
@@ -1023,35 +1599,35 @@ async function audit(options) {
1023
1599
  const errors = mergedIssues.filter((i) => i.severity === "error");
1024
1600
  const warnings = mergedIssues.filter((i) => i.severity === "warning");
1025
1601
  const notices = mergedIssues.filter((i) => i.severity === "notice");
1026
- console.log("\n" + chalk2.bold(" Issues by Severity:"));
1027
- console.log(` ${chalk2.red("\u25CF")} Errors: ${errors.length}`);
1028
- console.log(` ${chalk2.yellow("\u25CF")} Warnings: ${warnings.length}`);
1029
- console.log(` ${chalk2.blue("\u25CF")} Notices: ${notices.length}`);
1030
- console.log("\n" + chalk2.bold(" Top Issues:"));
1602
+ console.log("\n" + chalk4.bold(" Issues by Severity:"));
1603
+ console.log(` ${chalk4.red("\u25CF")} Errors: ${errors.length}`);
1604
+ console.log(` ${chalk4.yellow("\u25CF")} Warnings: ${warnings.length}`);
1605
+ console.log(` ${chalk4.blue("\u25CF")} Notices: ${notices.length}`);
1606
+ console.log("\n" + chalk4.bold(" Top Issues:"));
1031
1607
  const topIssues = [...errors, ...warnings, ...notices].slice(0, 10);
1032
1608
  topIssues.forEach((issue, i) => {
1033
- const icon = issue.severity === "error" ? chalk2.red("\u2717") : issue.severity === "warning" ? chalk2.yellow("!") : chalk2.blue("i");
1609
+ const icon = issue.severity === "error" ? chalk4.red("\u2717") : issue.severity === "warning" ? chalk4.yellow("!") : chalk4.blue("i");
1034
1610
  console.log(` ${i + 1}. ${icon} ${issue.title}`);
1035
1611
  if (issue.affectedUrls && issue.affectedUrls.length > 1) {
1036
- console.log(chalk2.dim(` Affects ${issue.affectedUrls.length} pages`));
1612
+ console.log(chalk4.dim(` Affects ${issue.affectedUrls.length} pages`));
1037
1613
  }
1038
1614
  });
1039
1615
  if (aiAnalysis) {
1040
- console.log("\n" + chalk2.bold(" AI Analysis:"));
1616
+ console.log("\n" + chalk4.bold(" AI Analysis:"));
1041
1617
  console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
1042
- console.log("\n " + chalk2.bold("Suggestions:"));
1618
+ console.log("\n " + chalk4.bold("Suggestions:"));
1043
1619
  aiAnalysis.suggestions.forEach((s, i) => {
1044
1620
  console.log(` ${i + 1}. ${s}`);
1045
1621
  });
1046
1622
  if (aiAnalysis.prioritizedFixes?.length > 0) {
1047
- console.log("\n " + chalk2.bold("Priority Fixes:"));
1623
+ console.log("\n " + chalk4.bold("Priority Fixes:"));
1048
1624
  aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix2, i) => {
1049
1625
  console.log(` ${i + 1}. [P${fix2.priority}] ${fix2.code}`);
1050
- console.log(chalk2.dim(` ${fix2.rationale}`));
1626
+ console.log(chalk4.dim(` ${fix2.rationale}`));
1051
1627
  });
1052
1628
  }
1053
1629
  }
1054
- console.log("\n" + chalk2.bold("\u2550".repeat(60)));
1630
+ console.log("\n" + chalk4.bold("\u2550".repeat(60)));
1055
1631
  const userInfo = getUserInfo();
1056
1632
  const tier = userInfo.planName?.toLowerCase() || "free";
1057
1633
  const isFreeTier = !loggedIn || tier === "free";
@@ -1067,13 +1643,13 @@ async function audit(options) {
1067
1643
  showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
1068
1644
  }
1069
1645
  if (loggedIn) {
1070
- const syncSpinner = ora("Syncing to cloud...").start();
1646
+ const syncSpinner = ora2("Syncing to cloud...").start();
1071
1647
  const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
1072
1648
  if (syncResult) {
1073
1649
  syncSpinner.succeed("Results synced to cloud");
1074
- console.log(chalk2.cyan(`
1650
+ console.log(chalk4.cyan(`
1075
1651
  View and manage issues in the web dashboard:`));
1076
- console.log(chalk2.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
1652
+ console.log(chalk4.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
1077
1653
  `));
1078
1654
  } else {
1079
1655
  syncSpinner.warn("Could not sync to cloud");
@@ -1090,8 +1666,8 @@ async function audit(options) {
1090
1666
  }
1091
1667
 
1092
1668
  // src/commands/apply.ts
1093
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1094
- import { join as join3 } from "path";
1669
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1670
+ import { join as join4 } from "path";
1095
1671
  import {
1096
1672
  runAuditWithFixes,
1097
1673
  generateFixes,
@@ -1125,11 +1701,11 @@ function truncate(str, maxLen) {
1125
1701
  }
1126
1702
  async function apply(options) {
1127
1703
  const cwd = process.cwd();
1128
- const configPath = join3(cwd, "seo-autopilot.config.json");
1704
+ const configPath = join4(cwd, "rankcli.config.json");
1129
1705
  let config2 = null;
1130
- if (existsSync3(configPath)) {
1706
+ if (existsSync4(configPath)) {
1131
1707
  try {
1132
- config2 = JSON.parse(readFileSync3(configPath, "utf-8"));
1708
+ config2 = JSON.parse(readFileSync4(configPath, "utf-8"));
1133
1709
  } catch {
1134
1710
  }
1135
1711
  }
@@ -1139,8 +1715,8 @@ async function apply(options) {
1139
1715
  }
1140
1716
  if (!url) {
1141
1717
  console.log("\n\u26A0\uFE0F Please provide a URL to analyze:");
1142
- console.log(" seo apply --url https://your-site.com");
1143
- console.log("\n Or set a URL in your seo-autopilot.config.json\n");
1718
+ console.log(" rankcli apply --url https://your-site.com");
1719
+ console.log("\n Or set a URL in your rankcli.config.json\n");
1144
1720
  return;
1145
1721
  }
1146
1722
  try {
@@ -1149,7 +1725,7 @@ async function apply(options) {
1149
1725
  console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
1150
1726
  return;
1151
1727
  }
1152
- console.log("\n\u{1F680} SEO Autopilot - Apply Fixes");
1728
+ console.log("\n\u{1F680} RankCLI - Apply Fixes");
1153
1729
  console.log("\u2550".repeat(50));
1154
1730
  const loggedIn = isLoggedIn();
1155
1731
  const userInfo = getUserInfo();
@@ -1175,11 +1751,18 @@ async function apply(options) {
1175
1751
  generateFixes: true
1176
1752
  });
1177
1753
  issues = auditResult.report.issues.map((issue) => ({
1178
- severity: issue.severity,
1754
+ // Squash the audit engine's 5-level severity into generateFixes's
1755
+ // 3-level SEOIssue scheme - matches how audit.ts already treats
1756
+ // 'error' as critical-weight (red ✗) and 'notice' as info-weight.
1757
+ severity: issue.severity === "error" ? "critical" : issue.severity === "notice" ? "info" : issue.severity,
1179
1758
  category: issue.category,
1180
1759
  code: issue.code,
1181
1760
  message: issue.title,
1182
1761
  impact: issue.description || void 0
1762
+ // Cast covers `category`: the audit engine's categories (17 values,
1763
+ // e.g. 'crawlability', 'links') don't map cleanly onto SEOIssue's
1764
+ // narrower set - generateFixes never reads category off its input
1765
+ // (each fixer hardcodes its own), so this is safe, not just silenced.
1183
1766
  }));
1184
1767
  score = auditResult.score;
1185
1768
  } catch (error) {
@@ -1254,7 +1837,7 @@ async function apply(options) {
1254
1837
  }
1255
1838
  if (config2 && !config2.url) {
1256
1839
  config2.url = url;
1257
- writeFileSync2(configPath, JSON.stringify(config2, null, 2));
1840
+ writeFileSync3(configPath, JSON.stringify(config2, null, 2));
1258
1841
  console.log("\n\u{1F4BE} Updated config with URL.");
1259
1842
  }
1260
1843
  console.log("\n" + "\u2500".repeat(50));
@@ -1265,7 +1848,7 @@ async function apply(options) {
1265
1848
  console.log("\n2. Commit the SEO improvements:");
1266
1849
  console.log(' git add -A && git commit -m "feat(seo): add meta tags and structured data"');
1267
1850
  console.log("\n3. Re-run the audit to verify:");
1268
- console.log(` seo audit --url ${url}`);
1851
+ console.log(` rankcli audit --url ${url}`);
1269
1852
  console.log("");
1270
1853
  }
1271
1854
 
@@ -1278,8 +1861,8 @@ import {
1278
1861
  applyFixes as applyFixes2,
1279
1862
  detectFramework as detectFramework4
1280
1863
  } from "@rankcli/agent-runtime";
1281
- import ora2 from "ora";
1282
- import chalk3 from "chalk";
1864
+ import ora3 from "ora";
1865
+ import chalk5 from "chalk";
1283
1866
  var execAsync = promisify(exec);
1284
1867
  var SEVERITY_ICON2 = {
1285
1868
  critical: "\u274C",
@@ -1297,8 +1880,7 @@ function generateCommitMessage(fixes, url) {
1297
1880
  (f) => f.issue.code.includes("MISSING") || f.issue.message.toLowerCase().includes("missing") || f.issue.message.toLowerCase().includes("add")
1298
1881
  );
1299
1882
  const type = isAddingNew ? "feat" : "fix";
1300
- const categories = new Set(applied.map((f) => f.issue.category || "seo"));
1301
- const categoryStr = categories.size === 1 ? Array.from(categories)[0] : "seo";
1883
+ const categoryStr = "seo";
1302
1884
  const issueCount = applied.length;
1303
1885
  const title = `${type}(${categoryStr}): fix ${issueCount} SEO issue${issueCount > 1 ? "s" : ""}`;
1304
1886
  const fixList = applied.map((f) => `- ${f.issue.code}: ${f.issue.message}`).join("\n");
@@ -1347,19 +1929,19 @@ function printFix2(fix2, index) {
1347
1929
  const icon = SEVERITY_ICON2[fix2.issue.severity] || "\u2022";
1348
1930
  console.log(`
1349
1931
  ${index + 1}. ${icon} ${fix2.issue.message}`);
1350
- console.log(` Code: ${chalk3.dim(fix2.issue.code)}`);
1351
- console.log(` File: ${chalk3.cyan(fix2.file)}`);
1932
+ console.log(` Code: ${chalk5.dim(fix2.issue.code)}`);
1933
+ console.log(` File: ${chalk5.cyan(fix2.file)}`);
1352
1934
  if (fix2.skipped) {
1353
- console.log(` Status: ${chalk3.yellow("SKIPPED")} - ${fix2.skipReason}`);
1935
+ console.log(` Status: ${chalk5.yellow("SKIPPED")} - ${fix2.skipReason}`);
1354
1936
  } else if (fix2.applied) {
1355
- console.log(` Status: ${chalk3.green("APPLIED")}`);
1356
- console.log(` ${chalk3.dim("Explanation:")} ${fix2.explanation}`);
1937
+ console.log(` Status: ${chalk5.green("APPLIED")}`);
1938
+ console.log(` ${chalk5.dim("Explanation:")} ${fix2.explanation}`);
1357
1939
  } else {
1358
- console.log(` ${chalk3.dim("Explanation:")} ${fix2.explanation}`);
1940
+ console.log(` ${chalk5.dim("Explanation:")} ${fix2.explanation}`);
1359
1941
  if (fix2.before) {
1360
- console.log(` ${chalk3.red("Before:")} ${truncate2(fix2.before, 60)}`);
1942
+ console.log(` ${chalk5.red("Before:")} ${truncate2(fix2.before, 60)}`);
1361
1943
  }
1362
- console.log(` ${chalk3.green("After:")} ${truncate2(fix2.after, 100)}`);
1944
+ console.log(` ${chalk5.green("After:")} ${truncate2(fix2.after, 100)}`);
1363
1945
  }
1364
1946
  }
1365
1947
  async function fix(options) {
@@ -1403,15 +1985,15 @@ async function fix(options) {
1403
1985
  console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
1404
1986
  console.log("\u2550".repeat(50));
1405
1987
  }
1406
- const frameworkSpinner = options.output !== "json" ? ora2("Detecting framework...").start() : null;
1988
+ const frameworkSpinner = options.output !== "json" ? ora3("Detecting framework...").start() : null;
1407
1989
  const frameworkResult = await detectFramework4({ cwd });
1408
1990
  const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
1409
1991
  frameworkSpinner?.succeed(`Framework: ${framework.name}`);
1410
- const analyzeSpinner = options.output !== "json" ? ora2(`Analyzing ${url}...`).start() : null;
1992
+ const analyzeSpinner = options.output !== "json" ? ora3(`Analyzing ${url}...`).start() : null;
1411
1993
  let issues;
1412
1994
  let score;
1413
1995
  try {
1414
- const analysis = await runDirectAnalysis2({ url });
1996
+ const analysis = await runDirectAnalysis2(url);
1415
1997
  issues = analysis.issues;
1416
1998
  score = analysis.score;
1417
1999
  } catch (error) {
@@ -1424,7 +2006,7 @@ async function fix(options) {
1424
2006
  const onlyCodes = new Set(options.only.map((c) => c.toUpperCase()));
1425
2007
  issues = issues.filter((i) => onlyCodes.has(i.code.toUpperCase()));
1426
2008
  if (options.output !== "json") {
1427
- console.log(chalk3.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
2009
+ console.log(chalk5.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
1428
2010
  }
1429
2011
  }
1430
2012
  if (issues.length === 0) {
@@ -1441,7 +2023,7 @@ async function fix(options) {
1441
2023
  durationMs: Date.now() - startTime
1442
2024
  };
1443
2025
  }
1444
- const fixSpinner = options.output !== "json" ? ora2("Generating fixes...").start() : null;
2026
+ const fixSpinner = options.output !== "json" ? ora3("Generating fixes...").start() : null;
1445
2027
  const fixes = await generateFixes2(issues, {
1446
2028
  cwd,
1447
2029
  url,
@@ -1453,13 +2035,13 @@ async function fix(options) {
1453
2035
  if (options.dryRun) {
1454
2036
  if (options.output !== "json") {
1455
2037
  console.log("\n" + "\u2500".repeat(50));
1456
- console.log(chalk3.yellow("DRY RUN - No changes will be made"));
2038
+ console.log(chalk5.yellow("DRY RUN - No changes will be made"));
1457
2039
  console.log("\u2500".repeat(50));
1458
2040
  for (let i = 0; i < fixes.length; i++) {
1459
2041
  printFix2(fixes[i], i);
1460
2042
  }
1461
2043
  console.log("\n" + "\u2500".repeat(50));
1462
- console.log(`\u{1F4CB} ${chalk3.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
2044
+ console.log(`\u{1F4CB} ${chalk5.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
1463
2045
  console.log(` Remove --dry-run to apply these fixes.
1464
2046
  `);
1465
2047
  }
@@ -1525,7 +2107,7 @@ async function fix(options) {
1525
2107
  durationMs: Date.now() - startTime
1526
2108
  };
1527
2109
  }
1528
- const applySpinner = options.output !== "json" ? ora2("Applying fixes...").start() : null;
2110
+ const applySpinner = options.output !== "json" ? ora3("Applying fixes...").start() : null;
1529
2111
  const { applied, skipped } = await applyFixes2(fixes, { cwd, dryRun: false });
1530
2112
  for (const fix2 of applied) {
1531
2113
  fix2.applied = true;
@@ -1537,7 +2119,7 @@ async function fix(options) {
1537
2119
  let commitError;
1538
2120
  let pushError;
1539
2121
  if (options.commit && applied.length > 0 && commitMessage) {
1540
- const gitSpinner = options.output !== "json" ? ora2("Committing changes...").start() : null;
2122
+ const gitSpinner = options.output !== "json" ? ora3("Committing changes...").start() : null;
1541
2123
  const gitResult = await executeGitCommit(cwd, commitMessage, options.push || false);
1542
2124
  committed = gitResult.committed;
1543
2125
  pushed = gitResult.pushed;
@@ -1563,23 +2145,23 @@ async function fix(options) {
1563
2145
  console.log("\u2550".repeat(50));
1564
2146
  if (applied.length > 0) {
1565
2147
  console.log(`
1566
- ${chalk3.green("\u2705")} Applied ${applied.length} fix(es):`);
2148
+ ${chalk5.green("\u2705")} Applied ${applied.length} fix(es):`);
1567
2149
  for (const fix2 of applied) {
1568
2150
  console.log(` - ${fix2.file}: ${fix2.issue.message}`);
1569
2151
  }
1570
2152
  }
1571
2153
  if (skipped.length > 0) {
1572
2154
  console.log(`
1573
- ${chalk3.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
2155
+ ${chalk5.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
1574
2156
  for (const fix2 of skipped) {
1575
2157
  console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
1576
2158
  }
1577
2159
  }
1578
2160
  if (committed) {
1579
2161
  console.log(`
1580
- ${chalk3.green("\u2705")} Changes committed`);
2162
+ ${chalk5.green("\u2705")} Changes committed`);
1581
2163
  if (pushed) {
1582
- console.log(`${chalk3.green("\u2705")} Changes pushed`);
2164
+ console.log(`${chalk5.green("\u2705")} Changes pushed`);
1583
2165
  }
1584
2166
  }
1585
2167
  if (!committed && applied.length > 0) {
@@ -1632,8 +2214,8 @@ ${chalk3.green("\u2705")} Changes committed`);
1632
2214
  }
1633
2215
 
1634
2216
  // src/commands/track.ts
1635
- import ora3 from "ora";
1636
- import chalk4 from "chalk";
2217
+ import ora4 from "ora";
2218
+ import chalk6 from "chalk";
1637
2219
  import { table } from "table";
1638
2220
  import { RankTracker, TIER_LIMITS as TIER_LIMITS2 } from "@rankcli/agent-runtime";
1639
2221
  async function getProjectForUrl(url) {
@@ -1672,35 +2254,35 @@ function getTierLimits(tier) {
1672
2254
  };
1673
2255
  }
1674
2256
  function formatPosition(pos) {
1675
- if (pos === null) return chalk4.dim("N/A");
1676
- if (pos <= 3) return chalk4.green.bold(`#${pos}`);
1677
- if (pos <= 10) return chalk4.green(`#${pos}`);
1678
- if (pos <= 20) return chalk4.yellow(`#${pos}`);
1679
- if (pos <= 50) return chalk4.red(`#${pos}`);
1680
- return chalk4.red.dim(`#${pos}`);
2257
+ if (pos === null) return chalk6.dim("N/A");
2258
+ if (pos <= 3) return chalk6.green.bold(`#${pos}`);
2259
+ if (pos <= 10) return chalk6.green(`#${pos}`);
2260
+ if (pos <= 20) return chalk6.yellow(`#${pos}`);
2261
+ if (pos <= 50) return chalk6.red(`#${pos}`);
2262
+ return chalk6.red.dim(`#${pos}`);
1681
2263
  }
1682
2264
  function formatChange(change) {
1683
- if (change === 0) return chalk4.dim("\u2192");
1684
- if (change > 0) return chalk4.green(`\u2191${change}`);
1685
- return chalk4.red(`\u2193${Math.abs(change)}`);
2265
+ if (change === 0) return chalk6.dim("\u2192");
2266
+ if (change > 0) return chalk6.green(`\u2191${change}`);
2267
+ return chalk6.red(`\u2193${Math.abs(change)}`);
1686
2268
  }
1687
2269
  async function trackAdd(options) {
1688
2270
  if (!options.keywords || options.keywords.length === 0) {
1689
- console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
2271
+ console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
1690
2272
  console.log(' rankcli track add "seo tool" "cli seo" --url https://your-site.com\n');
1691
2273
  return { success: false, error: "No keywords provided" };
1692
2274
  }
1693
2275
  if (!options.url) {
1694
- console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
2276
+ console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
1695
2277
  console.log(' rankcli track add "keyword" --url https://your-site.com\n');
1696
2278
  return { success: false, error: "No URL provided" };
1697
2279
  }
1698
2280
  if (!isLoggedIn()) {
1699
- console.log(chalk4.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
2281
+ console.log(chalk6.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
1700
2282
  console.log(" rankcli login\n");
1701
2283
  return { success: false, error: "Not logged in" };
1702
2284
  }
1703
- const spinner = ora3("Adding keywords to track...").start();
2285
+ const spinner = ora4("Adding keywords to track...").start();
1704
2286
  try {
1705
2287
  const project = await getProjectForUrl(options.url);
1706
2288
  if (!project) {
@@ -1720,7 +2302,7 @@ async function trackAdd(options) {
1720
2302
  const newCount = options.keywords.length;
1721
2303
  if (existingCount + newCount > maxKeywords) {
1722
2304
  spinner.fail(`Keyword limit reached (${existingCount}/${maxKeywords})`);
1723
- showUpsell("keyword_limit", { tier, current: existingCount, limit: maxKeywords });
2305
+ showUpsell("keyword_limit", { tier, current: existingCount, max: maxKeywords });
1724
2306
  return { success: false, error: `Keyword limit reached. Upgrade to track more keywords.` };
1725
2307
  }
1726
2308
  const keywordRecords = options.keywords.map((keyword) => ({
@@ -1741,10 +2323,10 @@ async function trackAdd(options) {
1741
2323
  }
1742
2324
  const addedCount = data?.length || 0;
1743
2325
  spinner.succeed(`Added ${addedCount} keyword(s) to track`);
1744
- console.log(chalk4.dim(`
2326
+ console.log(chalk6.dim(`
1745
2327
  Tracking: ${options.keywords.join(", ")}`));
1746
- console.log(chalk4.dim(`For: ${project.domain}`));
1747
- console.log(chalk4.cyan(`
2328
+ console.log(chalk6.dim(`For: ${project.domain}`));
2329
+ console.log(chalk6.cyan(`
1748
2330
  Run 'rankcli track check --url ${options.url}' to check current positions.
1749
2331
  `));
1750
2332
  return { success: true, addedCount };
@@ -1755,16 +2337,16 @@ Run 'rankcli track check --url ${options.url}' to check current positions.
1755
2337
  }
1756
2338
  async function trackStatus(options) {
1757
2339
  if (!options.url) {
1758
- console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide a URL:"));
2340
+ console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a URL:"));
1759
2341
  console.log(" rankcli track status --url https://your-site.com\n");
1760
2342
  return { success: false, error: "No URL provided" };
1761
2343
  }
1762
2344
  if (!isLoggedIn()) {
1763
- console.log(chalk4.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
2345
+ console.log(chalk6.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
1764
2346
  console.log(" rankcli login\n");
1765
2347
  return { success: false, error: "Not logged in" };
1766
2348
  }
1767
- const spinner = ora3("Loading keyword rankings...").start();
2349
+ const spinner = ora4("Loading keyword rankings...").start();
1768
2350
  try {
1769
2351
  const project = await getProjectForUrl(options.url);
1770
2352
  if (!project) {
@@ -1783,8 +2365,8 @@ async function trackStatus(options) {
1783
2365
  }
1784
2366
  spinner.succeed(`Loaded ${data?.length || 0} tracked keywords`);
1785
2367
  if (!data || data.length === 0) {
1786
- console.log(chalk4.dim("\nNo keywords tracked yet."));
1787
- console.log(chalk4.cyan(`Run 'rankcli track add "keyword" --url ${options.url}' to start tracking.
2368
+ console.log(chalk6.dim("\nNo keywords tracked yet."));
2369
+ console.log(chalk6.cyan(`Run 'rankcli track add "keyword" --url ${options.url}' to start tracking.
1788
2370
  `));
1789
2371
  return { success: true, keywords: [] };
1790
2372
  }
@@ -1799,7 +2381,7 @@ async function trackStatus(options) {
1799
2381
  if (options.output === "json") {
1800
2382
  console.log(JSON.stringify({ keywords: keywords2 }, null, 2));
1801
2383
  } else {
1802
- console.log("\n" + chalk4.bold("\u{1F4CA} Keyword Rankings"));
2384
+ console.log("\n" + chalk6.bold("\u{1F4CA} Keyword Rankings"));
1803
2385
  console.log("\u2550".repeat(60));
1804
2386
  const tableData = [
1805
2387
  ["Keyword", "Position", "Best", "Change", "Last Checked"],
@@ -1842,14 +2424,14 @@ async function trackHistory(options) {
1842
2424
  return { success: false, error: "No URL provided" };
1843
2425
  }
1844
2426
  if (!options.keyword) {
1845
- console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
2427
+ console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
1846
2428
  console.log(' rankcli track history --keyword "seo tool" --url https://your-site.com\n');
1847
2429
  return { success: false, error: "No keyword provided" };
1848
2430
  }
1849
2431
  if (!isLoggedIn()) {
1850
2432
  return { success: false, error: "Not logged in" };
1851
2433
  }
1852
- const spinner = ora3(`Loading history for "${options.keyword}"...`).start();
2434
+ const spinner = ora4(`Loading history for "${options.keyword}"...`).start();
1853
2435
  try {
1854
2436
  const project = await getProjectForUrl(options.url);
1855
2437
  if (!project) {
@@ -1882,21 +2464,21 @@ async function trackHistory(options) {
1882
2464
  if (options.output === "json") {
1883
2465
  console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
1884
2466
  } else {
1885
- console.log("\n" + chalk4.bold(`\u{1F4C8} History: "${options.keyword}"`));
2467
+ console.log("\n" + chalk6.bold(`\u{1F4C8} History: "${options.keyword}"`));
1886
2468
  console.log("\u2550".repeat(50));
1887
2469
  if (history.length === 0) {
1888
- console.log(chalk4.dim("\nNo history available yet.\n"));
2470
+ console.log(chalk6.dim("\nNo history available yet.\n"));
1889
2471
  } else {
1890
2472
  const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
1891
2473
  const sparkline = positions.map((p) => {
1892
- if (p <= 10) return chalk4.green("\u2588");
1893
- if (p <= 20) return chalk4.yellow("\u2593");
1894
- if (p <= 50) return chalk4.red("\u2592");
1895
- return chalk4.dim("\u2591");
2474
+ if (p <= 10) return chalk6.green("\u2588");
2475
+ if (p <= 20) return chalk6.yellow("\u2593");
2476
+ if (p <= 50) return chalk6.red("\u2592");
2477
+ return chalk6.dim("\u2591");
1896
2478
  }).join("");
1897
2479
  console.log(`
1898
2480
  ${sparkline} (last ${positions.length} checks)`);
1899
- console.log(chalk4.dim(" \u2190 newer older \u2192\n"));
2481
+ console.log(chalk6.dim(" \u2190 newer older \u2192\n"));
1900
2482
  const recent = history.slice(0, 10);
1901
2483
  for (const entry of recent) {
1902
2484
  console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
@@ -1916,7 +2498,7 @@ async function trackExport(options) {
1916
2498
  if (!isLoggedIn()) {
1917
2499
  return { success: false, error: "Not logged in" };
1918
2500
  }
1919
- const spinner = ora3("Exporting data...").start();
2501
+ const spinner = ora4("Exporting data...").start();
1920
2502
  try {
1921
2503
  const project = await getProjectForUrl(options.url);
1922
2504
  if (!project) {
@@ -1973,12 +2555,12 @@ async function trackExport(options) {
1973
2555
  }
1974
2556
  async function trackCheck(options) {
1975
2557
  if (!options.url) {
1976
- console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide a URL:"));
2558
+ console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a URL:"));
1977
2559
  console.log(" rankcli track check --url https://your-site.com\n");
1978
2560
  return { success: false, error: "No URL provided" };
1979
2561
  }
1980
2562
  if (!isLoggedIn()) {
1981
- console.log(chalk4.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
2563
+ console.log(chalk6.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
1982
2564
  console.log(" rankcli login\n");
1983
2565
  return { success: false, error: "Not logged in" };
1984
2566
  }
@@ -1995,14 +2577,14 @@ async function trackCheck(options) {
1995
2577
  const minHoursBetweenChecks = 24 / checksPerDay;
1996
2578
  if (hoursSinceCheck < minHoursBetweenChecks) {
1997
2579
  const nextCheck = new Date(project.lastRankCheckAt.getTime() + minHoursBetweenChecks * 60 * 60 * 1e3);
1998
- console.log(chalk4.yellow(`
2580
+ console.log(chalk6.yellow(`
1999
2581
  \u26A0\uFE0F Rate limited. Next check available: ${nextCheck.toLocaleString()}`));
2000
- console.log(chalk4.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
2582
+ console.log(chalk6.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
2001
2583
  `));
2002
2584
  return { success: true, rateLimited: true };
2003
2585
  }
2004
2586
  }
2005
- const spinner = ora3("Checking current rankings...").start();
2587
+ const spinner = ora4("Checking current rankings...").start();
2006
2588
  const supabase = await getAuthenticatedClient();
2007
2589
  if (!supabase) {
2008
2590
  spinner.fail("Could not connect to database");
@@ -2016,8 +2598,8 @@ async function trackCheck(options) {
2016
2598
  const serpApiKey = process.env.VALUESERP_API_KEY || process.env.SERPAPI_KEY;
2017
2599
  if (!serpApiKey) {
2018
2600
  spinner.warn("No SERP API key configured");
2019
- console.log(chalk4.yellow("\n\u26A0\uFE0F Set VALUESERP_API_KEY or SERPAPI_KEY to check live rankings."));
2020
- console.log(chalk4.dim(" Get a key at https://www.valueserp.com or https://serpapi.com\n"));
2601
+ console.log(chalk6.yellow("\n\u26A0\uFE0F Set VALUESERP_API_KEY or SERPAPI_KEY to check live rankings."));
2602
+ console.log(chalk6.dim(" Get a key at https://www.valueserp.com or https://serpapi.com\n"));
2021
2603
  return { success: false, error: "No SERP API key" };
2022
2604
  }
2023
2605
  const tracker = new RankTracker({
@@ -2029,13 +2611,13 @@ async function trackCheck(options) {
2029
2611
  });
2030
2612
  const results = await tracker.checkRankings(project.id, project.domain);
2031
2613
  spinner.succeed(`Checked ${results.length} keyword(s)`);
2032
- console.log("\n" + chalk4.bold("\u{1F50D} Rank Check Results"));
2614
+ console.log("\n" + chalk6.bold("\u{1F50D} Rank Check Results"));
2033
2615
  console.log("\u2550".repeat(50));
2034
2616
  for (const result of results) {
2035
2617
  const posStr = formatPosition(result.position);
2036
2618
  console.log(` ${result.keyword}: ${posStr}`);
2037
2619
  if (result.serpFeatures.length > 0) {
2038
- console.log(chalk4.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
2620
+ console.log(chalk6.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
2039
2621
  }
2040
2622
  }
2041
2623
  console.log("");
@@ -2054,8 +2636,8 @@ async function trackCheck(options) {
2054
2636
 
2055
2637
  // src/commands/geo.ts
2056
2638
  import { analyzers } from "@rankcli/agent-runtime";
2057
- import ora4 from "ora";
2058
- import chalk5 from "chalk";
2639
+ import ora5 from "ora";
2640
+ import chalk7 from "chalk";
2059
2641
  async function fetchResources(url) {
2060
2642
  const headers = {};
2061
2643
  let html = null;
@@ -2101,7 +2683,7 @@ async function geo(options) {
2101
2683
  console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
2102
2684
  return;
2103
2685
  }
2104
- const spinner = ora4("Fetching page and robots.txt...").start();
2686
+ const spinner = ora5("Fetching page and robots.txt...").start();
2105
2687
  const { html, robotsTxt, headers } = await fetchResources(url);
2106
2688
  if (!html) {
2107
2689
  spinner.fail("Could not fetch page content");
@@ -2127,55 +2709,55 @@ async function geo(options) {
2127
2709
  console.log(JSON.stringify(jsonOutput, null, 2));
2128
2710
  return;
2129
2711
  }
2130
- console.log("\n" + chalk5.bold("\u2550".repeat(60)));
2131
- console.log(chalk5.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
2132
- console.log(chalk5.bold("\u2550".repeat(60)));
2712
+ console.log("\n" + chalk7.bold("\u2550".repeat(60)));
2713
+ console.log(chalk7.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
2714
+ console.log(chalk7.bold("\u2550".repeat(60)));
2133
2715
  const geoScore = geoResult.score;
2134
- const scoreColor = geoScore >= 80 ? chalk5.green : geoScore >= 60 ? chalk5.yellow : chalk5.red;
2716
+ const scoreColor = geoScore >= 80 ? chalk7.green : geoScore >= 60 ? chalk7.yellow : chalk7.red;
2135
2717
  console.log(`
2136
- ${chalk5.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
2137
- console.log("\n" + chalk5.bold(" AI Crawler Access:"));
2718
+ ${chalk7.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
2719
+ console.log("\n" + chalk7.bold(" AI Crawler Access:"));
2138
2720
  const { aiCrawlerAccess } = geoResult;
2139
2721
  if (aiCrawlerAccess.blockedCrawlers.length > 0) {
2140
- console.log(chalk5.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
2722
+ console.log(chalk7.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
2141
2723
  }
2142
2724
  if (aiCrawlerAccess.allowedCrawlers.length > 0) {
2143
2725
  const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
2144
- console.log(chalk5.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
2726
+ console.log(chalk7.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
2145
2727
  }
2146
- console.log("\n" + chalk5.bold(" Rendering:"));
2728
+ console.log("\n" + chalk7.bold(" Rendering:"));
2147
2729
  if (aiCrawlerAccess.serverSideRendered) {
2148
- console.log(chalk5.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
2730
+ console.log(chalk7.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
2149
2731
  } else if (aiCrawlerAccess.jsRenderingRequired) {
2150
- console.log(chalk5.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
2732
+ console.log(chalk7.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
2151
2733
  }
2152
- console.log("\n" + chalk5.bold(" LLM Friendliness:"));
2734
+ console.log("\n" + chalk7.bold(" LLM Friendliness:"));
2153
2735
  const { llmSignals } = geoResult;
2154
2736
  const bar = (score) => {
2155
2737
  const filled = Math.round(score / 5);
2156
2738
  const empty = 20 - filled;
2157
- const color = score >= 70 ? chalk5.green : score >= 40 ? chalk5.yellow : chalk5.red;
2158
- return color("\u2588".repeat(filled)) + chalk5.gray("\u2591".repeat(empty));
2739
+ const color = score >= 70 ? chalk7.green : score >= 40 ? chalk7.yellow : chalk7.red;
2740
+ return color("\u2588".repeat(filled)) + chalk7.gray("\u2591".repeat(empty));
2159
2741
  };
2160
2742
  console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
2161
2743
  console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
2162
2744
  console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
2163
2745
  console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
2164
- console.log("\n" + chalk5.bold(" Content Structure:"));
2746
+ console.log("\n" + chalk7.bold(" Content Structure:"));
2165
2747
  const { contentStructure } = geoResult;
2166
- console.log(` ${contentStructure.hasStructuredData ? chalk5.green("\u2705") : chalk5.red("\u274C")} JSON-LD Structured Data`);
2167
- console.log(` ${contentStructure.hasFAQSchema ? chalk5.green("\u2705") : chalk5.yellow("\u25CB")} FAQ Schema`);
2168
- console.log(` ${contentStructure.hasArticleSchema ? chalk5.green("\u2705") : chalk5.yellow("\u25CB")} Article Schema`);
2169
- console.log(` ${contentStructure.hasBreadcrumbs ? chalk5.green("\u2705") : chalk5.yellow("\u25CB")} Breadcrumbs`);
2170
- console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? chalk5.green("Good") : contentStructure.headingHierarchy === "needs-work" ? chalk5.yellow("Needs Work") : chalk5.red("Poor")}`);
2171
- console.log("\n" + chalk5.bold(" Citation Readiness:"));
2748
+ console.log(` ${contentStructure.hasStructuredData ? chalk7.green("\u2705") : chalk7.red("\u274C")} JSON-LD Structured Data`);
2749
+ console.log(` ${contentStructure.hasFAQSchema ? chalk7.green("\u2705") : chalk7.yellow("\u25CB")} FAQ Schema`);
2750
+ console.log(` ${contentStructure.hasArticleSchema ? chalk7.green("\u2705") : chalk7.yellow("\u25CB")} Article Schema`);
2751
+ console.log(` ${contentStructure.hasBreadcrumbs ? chalk7.green("\u2705") : chalk7.yellow("\u25CB")} Breadcrumbs`);
2752
+ console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? chalk7.green("Good") : contentStructure.headingHierarchy === "needs-work" ? chalk7.yellow("Needs Work") : chalk7.red("Poor")}`);
2753
+ console.log("\n" + chalk7.bold(" Citation Readiness:"));
2172
2754
  const { citationReadiness } = geoResult;
2173
2755
  if (citationReadiness.trustSignals.length > 0) {
2174
- console.log(chalk5.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
2756
+ console.log(chalk7.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
2175
2757
  } else {
2176
- console.log(chalk5.yellow(" \u25CB No trust signals detected"));
2758
+ console.log(chalk7.yellow(" \u25CB No trust signals detected"));
2177
2759
  }
2178
- console.log("\n" + chalk5.bold(" Other Scores:"));
2760
+ console.log("\n" + chalk7.bold(" Other Scores:"));
2179
2761
  console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
2180
2762
  console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
2181
2763
  console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
@@ -2189,18 +2771,18 @@ async function geo(options) {
2189
2771
  ];
2190
2772
  const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
2191
2773
  if (criticalIssues.length > 0) {
2192
- console.log("\n" + chalk5.bold.red(" Critical Issues:"));
2774
+ console.log("\n" + chalk7.bold.red(" Critical Issues:"));
2193
2775
  criticalIssues.slice(0, 5).forEach((issue, i) => {
2194
- console.log(chalk5.red(` ${i + 1}. ${issue.title}`));
2776
+ console.log(chalk7.red(` ${i + 1}. ${issue.title}`));
2195
2777
  });
2196
2778
  }
2197
2779
  if (geoResult.recommendations.length > 0) {
2198
- console.log("\n" + chalk5.bold(" Recommendations:"));
2780
+ console.log("\n" + chalk7.bold(" Recommendations:"));
2199
2781
  geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
2200
2782
  console.log(` ${i + 1}. ${rec}`);
2201
2783
  });
2202
2784
  }
2203
- console.log("\n" + chalk5.bold("\u2550".repeat(60)));
2785
+ console.log("\n" + chalk7.bold("\u2550".repeat(60)));
2204
2786
  console.log("");
2205
2787
  } catch (error) {
2206
2788
  spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -2208,7 +2790,7 @@ async function geo(options) {
2208
2790
  }
2209
2791
 
2210
2792
  // src/commands/keywords.ts
2211
- import * as readline from "readline";
2793
+ import * as readline2 from "readline";
2212
2794
  import {
2213
2795
  runKeywordResearch,
2214
2796
  formatKeywordReport,
@@ -2221,10 +2803,10 @@ import {
2221
2803
  runAIKeywordResearch
2222
2804
  } from "@rankcli/agent-runtime";
2223
2805
  async function keywords(options) {
2224
- console.log("\n\u{1F511} SEO Autopilot - Keyword Research\n");
2806
+ console.log("\n\u{1F511} RankCLI - Keyword Research\n");
2225
2807
  console.log("This tool helps you find keywords you can actually rank for,");
2226
2808
  console.log("based on your site's current authority and competition level.\n");
2227
- const rl = readline.createInterface({
2809
+ const rl = readline2.createInterface({
2228
2810
  input: process.stdin,
2229
2811
  output: process.stdout
2230
2812
  });
@@ -2590,10 +3172,12 @@ import {
2590
3172
  formatFeaturedSnippetReport,
2591
3173
  analyzeKeywordDensity,
2592
3174
  formatKeywordDensityReport,
2593
- crawlUrl
3175
+ crawlUrl,
3176
+ extractMeta,
3177
+ analyzeHeadings
2594
3178
  } from "@rankcli/agent-runtime";
2595
3179
  async function analyzeContent(options) {
2596
- console.log("\n\u{1F4DD} SEO Autopilot - Content Analysis\n");
3180
+ console.log("\n\u{1F4DD} RankCLI - Content Analysis\n");
2597
3181
  const mode = options.mode || "full";
2598
3182
  if (mode === "headline" || options.headline) {
2599
3183
  const headline = options.headline || "";
@@ -2617,15 +3201,22 @@ async function analyzeContent(options) {
2617
3201
  if (options.url) {
2618
3202
  console.log(`Fetching content from ${options.url}...`);
2619
3203
  try {
2620
- const result = await crawlUrl(options.url);
2621
- if (!result.html) {
3204
+ const crawlResult = await crawlUrl({ url: options.url });
3205
+ if (!crawlResult.success || !crawlResult.data) {
2622
3206
  console.log("\u274C Could not fetch content from URL");
2623
3207
  return;
2624
3208
  }
2625
- const title = result.meta?.title || "";
2626
- const h1 = result.headings?.find((h) => h.level === 1)?.text || "";
2627
- const headings = result.headings?.map((h) => h.text) || [];
2628
- const bodyText = extractBodyText(result.html);
3209
+ const { html } = crawlResult.data;
3210
+ const [metaResult, headingsResult] = await Promise.all([
3211
+ extractMeta({ html, url: options.url }),
3212
+ analyzeHeadings({ html })
3213
+ ]);
3214
+ const meta = metaResult.success ? metaResult.data : void 0;
3215
+ const headingStructure = headingsResult.success ? headingsResult.data : [];
3216
+ const title = meta?.title || "";
3217
+ const h1 = headingStructure.find((h) => h.level === 1)?.text || "";
3218
+ const headings = headingStructure.map((h) => h.text);
3219
+ const bodyText = extractBodyText(html);
2629
3220
  const paragraphs = bodyText.split(/\n\n+/);
2630
3221
  console.log(`
2631
3222
  Analyzing: "${title || options.url}"
@@ -2667,215 +3258,39 @@ Analyzing: "${title || options.url}"
2667
3258
  console.log(` H1: ${h1 || "(none)"}`);
2668
3259
  console.log(` Word Count: ${bodyText.split(/\s+/).length}`);
2669
3260
  console.log(` Headings: ${headings.length}`);
2670
- console.log("");
2671
- } catch (error) {
2672
- console.log(`
2673
- \u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
2674
- `);
2675
- }
2676
- } else {
2677
- console.log("Usage:");
2678
- console.log(" seo content -u <url> Full content analysis");
2679
- console.log(' seo content -u <url> -k "keyword" Analyze with target keyword');
2680
- console.log(' seo content --headline "Your Title" Analyze headline only');
2681
- console.log(" seo content -u <url> --mode readability Readability only");
2682
- console.log(' seo content -u <url> -k "kw" --mode density Keyword density only');
2683
- console.log(' seo content -u <url> -k "kw" --mode snippet Featured snippet analysis');
2684
- console.log("");
2685
- }
2686
- }
2687
- async function analyzeIntent(keyword) {
2688
- console.log("\n\u{1F50D} Search Intent Analysis\n");
2689
- const analysis = classifyIntent(keyword);
2690
- console.log(formatIntentReport(keyword, analysis));
2691
- }
2692
- function extractBodyText(html) {
2693
- let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
2694
- text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
2695
- text = text.replace(/<[^>]+>/g, " ");
2696
- text = text.replace(/&nbsp;/g, " ");
2697
- text = text.replace(/&amp;/g, "&");
2698
- text = text.replace(/&lt;/g, "<");
2699
- text = text.replace(/&gt;/g, ">");
2700
- text = text.replace(/&quot;/g, '"');
2701
- text = text.replace(/\s+/g, " ").trim();
2702
- return text;
2703
- }
2704
-
2705
- // src/commands/setup.ts
2706
- import * as readline2 from "readline";
2707
- import * as fs from "fs";
2708
- import * as path from "path";
2709
- import {
2710
- injectGA4,
2711
- injectGSCVerification,
2712
- writeGitHubActionFiles,
2713
- getGSCSetupInstructions,
2714
- generateGA4EnvTemplate
2715
- } from "@rankcli/agent-runtime";
2716
- async function setup(options) {
2717
- console.log("\n\u{1F527} SEO Autopilot - Setup\n");
2718
- const projectPath = process.cwd();
2719
- if (!fs.existsSync(path.join(projectPath, "package.json")) && !fs.existsSync(path.join(projectPath, "index.html"))) {
2720
- console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
2721
- }
2722
- if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
2723
- await interactiveSetup(projectPath);
2724
- return;
2725
- }
2726
- if (options.ga4) {
2727
- await setupGA4(projectPath, options.ga4);
2728
- }
2729
- if (options.gsc) {
2730
- await setupGSC(projectPath, options.gsc);
2731
- }
2732
- if (options.githubAction) {
2733
- await setupGitHubAction(projectPath, options.schedule || "weekly");
2734
- }
2735
- }
2736
- async function interactiveSetup(projectPath) {
2737
- const rl = readline2.createInterface({
2738
- input: process.stdin,
2739
- output: process.stdout
2740
- });
2741
- const ask2 = (question) => {
2742
- return new Promise((resolve) => {
2743
- rl.question(question, (answer) => {
2744
- resolve(answer.trim());
2745
- });
2746
- });
2747
- };
2748
- try {
2749
- console.log("This wizard will help you set up SEO tracking for your project.\n");
2750
- console.log("\u2500".repeat(60));
2751
- console.log("\u{1F4CA} STEP 1: Google Analytics 4\n");
2752
- console.log("GA4 tracks visitor behavior on your site.");
2753
- console.log("Get your Measurement ID from: https://analytics.google.com");
2754
- console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
2755
- const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
2756
- if (ga4Id && ga4Id.startsWith("G-")) {
2757
- await setupGA4(projectPath, ga4Id);
2758
- } else if (ga4Id) {
2759
- console.log('\u26A0\uFE0F Invalid format. GA4 IDs start with "G-". Skipping.\n');
2760
- } else {
2761
- console.log("Skipping GA4 setup.\n");
2762
- }
2763
- console.log("\u2500".repeat(60));
2764
- console.log("\u{1F50D} STEP 2: Google Search Console\n");
2765
- console.log("GSC shows how Google sees your site and what queries you rank for.");
2766
- console.log("Get your verification code from: https://search.google.com/search-console");
2767
- console.log("Go to: Settings > Ownership verification > HTML tag\n");
2768
- const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
2769
- if (gscCode && gscCode.length > 10) {
2770
- await setupGSC(projectPath, gscCode);
2771
- } else if (gscCode) {
2772
- console.log("\u26A0\uFE0F Code seems too short. Skipping.\n");
2773
- } else {
2774
- console.log("Skipping GSC verification.\n");
2775
- }
2776
- console.log("\u2500".repeat(60));
2777
- console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
2778
- console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
2779
- console.log("It creates issues with reports and can auto-fix some problems.\n");
2780
- const setupAction = await ask2("Set up GitHub Action? (y/n): ");
2781
- if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
2782
- console.log("\nHow often should the check run?");
2783
- console.log(" 1. Weekly (recommended for most sites)");
2784
- console.log(" 2. Daily (for high-traffic sites)");
2785
- console.log(" 3. Monthly (for low-traffic sites)\n");
2786
- const scheduleChoice = await ask2("Enter choice (1-3): ");
2787
- const scheduleMap = {
2788
- "1": "weekly",
2789
- "2": "daily",
2790
- "3": "monthly"
2791
- };
2792
- const schedule = scheduleMap[scheduleChoice] || "weekly";
2793
- const siteUrl = await ask2("Enter your site URL (https://example.com): ");
2794
- if (siteUrl) {
2795
- await setupGitHubAction(projectPath, schedule, siteUrl);
2796
- }
2797
- } else {
2798
- console.log("Skipping GitHub Action setup.\n");
2799
- }
2800
- console.log("\u2500".repeat(60));
2801
- console.log("\u2705 Setup complete!\n");
2802
- console.log("Next steps:");
2803
- console.log(" 1. Commit the changes to your repository");
2804
- console.log(" 2. If using GitHub Actions, add required secrets (see .github/SEO_SETUP.md)");
2805
- console.log(" 3. Deploy your site to apply tracking code changes");
2806
- console.log(" 4. Verify GSC ownership and GA4 data collection\n");
2807
- rl.close();
2808
- } catch (error) {
2809
- console.log(`
3261
+ console.log("");
3262
+ } catch (error) {
3263
+ console.log(`
2810
3264
  \u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
2811
3265
  `);
2812
- rl.close();
2813
- }
2814
- }
2815
- async function setupGA4(projectPath, measurementId) {
2816
- console.log(`Setting up Google Analytics 4 (${measurementId})...`);
2817
- const result = await injectGA4(projectPath, { measurementId });
2818
- if (result.success) {
2819
- console.log(`\u2705 ${result.message}`);
2820
- if (result.file) {
2821
- console.log(` Modified: ${result.file}`);
2822
- }
2823
- } else {
2824
- console.log(`\u26A0\uFE0F ${result.message}`);
2825
- if (result.code) {
2826
- console.log("\nManually add this code to your HTML <head>:\n");
2827
- console.log(result.code);
2828
- }
2829
- }
2830
- const envExamplePath = path.join(projectPath, ".env.example");
2831
- const envContent = generateGA4EnvTemplate();
2832
- if (fs.existsSync(envExamplePath)) {
2833
- const existing = fs.readFileSync(envExamplePath, "utf-8");
2834
- if (!existing.includes("GA4_MEASUREMENT_ID")) {
2835
- fs.appendFileSync(envExamplePath, "\n" + envContent);
2836
- console.log(" Updated: .env.example");
2837
- }
2838
- } else {
2839
- fs.writeFileSync(envExamplePath, envContent);
2840
- console.log(" Created: .env.example");
2841
- }
2842
- console.log("");
2843
- }
2844
- async function setupGSC(projectPath, verificationCode) {
2845
- console.log("Setting up Google Search Console verification...");
2846
- const result = await injectGSCVerification(projectPath, verificationCode);
2847
- if (result.success) {
2848
- console.log(`\u2705 ${result.message}`);
2849
- if (result.file) {
2850
- console.log(` Modified: ${result.file}`);
2851
3266
  }
2852
3267
  } else {
2853
- console.log(`\u26A0\uFE0F ${result.message}`);
3268
+ console.log("Usage:");
3269
+ console.log(" seo content -u <url> Full content analysis");
3270
+ console.log(' seo content -u <url> -k "keyword" Analyze with target keyword');
3271
+ console.log(' seo content --headline "Your Title" Analyze headline only');
3272
+ console.log(" seo content -u <url> --mode readability Readability only");
3273
+ console.log(' seo content -u <url> -k "kw" --mode density Keyword density only');
3274
+ console.log(' seo content -u <url> -k "kw" --mode snippet Featured snippet analysis');
3275
+ console.log("");
2854
3276
  }
2855
- console.log("");
2856
3277
  }
2857
- async function setupGitHubAction(projectPath, schedule, siteUrl) {
2858
- console.log(`Setting up GitHub Action (${schedule} schedule)...`);
2859
- const config2 = {
2860
- schedule,
2861
- siteUrl: siteUrl || "https://example.com",
2862
- features: {
2863
- audit: true,
2864
- tracking: true,
2865
- autoFix: true,
2866
- createIssues: true,
2867
- createPRs: true
2868
- }
2869
- };
2870
- const { files, instructions } = writeGitHubActionFiles(projectPath, config2);
2871
- console.log("\u2705 GitHub Action files created:");
2872
- for (const file of files) {
2873
- console.log(` - ${path.relative(projectPath, file)}`);
2874
- }
2875
- console.log(instructions);
3278
+ async function analyzeIntent(keyword) {
3279
+ console.log("\n\u{1F50D} Search Intent Analysis\n");
3280
+ const analysis = classifyIntent(keyword);
3281
+ console.log(formatIntentReport(keyword, analysis));
2876
3282
  }
2877
- function showGSCInstructions() {
2878
- console.log(getGSCSetupInstructions());
3283
+ function extractBodyText(html) {
3284
+ let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
3285
+ text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
3286
+ text = text.replace(/<[^>]+>/g, " ");
3287
+ text = text.replace(/&nbsp;/g, " ");
3288
+ text = text.replace(/&amp;/g, "&");
3289
+ text = text.replace(/&lt;/g, "<");
3290
+ text = text.replace(/&gt;/g, ">");
3291
+ text = text.replace(/&quot;/g, '"');
3292
+ text = text.replace(/\s+/g, " ").trim();
3293
+ return text;
2879
3294
  }
2880
3295
 
2881
3296
  // src/commands/alerts.ts
@@ -2900,7 +3315,7 @@ async function askYesNo(rl, question, defaultYes = true) {
2900
3315
  return answer.toLowerCase().startsWith("y");
2901
3316
  }
2902
3317
  async function alerts(options) {
2903
- console.log("\n\u{1F514} SEO Autopilot - Alert Configuration\n");
3318
+ console.log("\n\u{1F514} RankCLI - Alert Configuration\n");
2904
3319
  const rl = readline3.createInterface({
2905
3320
  input: process.stdin,
2906
3321
  output: process.stdout
@@ -3043,235 +3458,18 @@ async function testWebhook(channel, webhookUrl) {
3043
3458
  }
3044
3459
  }
3045
3460
 
3046
- // src/commands/auth.ts
3047
- import chalk6 from "chalk";
3048
- import ora5 from "ora";
3049
- import inquirer from "inquirer";
3050
- import open from "open";
3051
- async function login(options) {
3052
- if (isLoggedIn()) {
3053
- const { email } = getUserInfo();
3054
- console.log(chalk6.yellow(`Already logged in as ${email}`));
3055
- console.log(chalk6.dim('Run "rankcli logout" to sign out first.'));
3056
- return;
3057
- }
3058
- const spinner = ora5();
3059
- try {
3060
- if (options.token) {
3061
- const apiKey = options.token;
3062
- if (!apiKey.startsWith("rankcli_")) {
3063
- console.log(chalk6.red('Invalid API key format. Keys should start with "rankcli_"'));
3064
- return;
3065
- }
3066
- spinner.start("Validating API key...");
3067
- const result = await validateApiKey(apiKey);
3068
- if (!result.valid) {
3069
- spinner.fail("Invalid API key");
3070
- console.log(chalk6.red(`
3071
- ${result.error}`));
3072
- return;
3073
- }
3074
- saveApiKey(apiKey);
3075
- if (result.subscription) {
3076
- saveSubscription({
3077
- planId: result.subscription.planId,
3078
- planName: result.subscription.planName,
3079
- sitesLimit: result.subscription.sitesLimit
3080
- });
3081
- }
3082
- spinner.succeed(`Authenticated via API key`);
3083
- console.log(chalk6.dim(`
3084
- Email: ${result.user?.email}`));
3085
- console.log(chalk6.dim(`Plan: ${result.subscription?.planName || "Free"}`));
3086
- return;
3087
- }
3088
- if (options.browser) {
3089
- console.log(chalk6.cyan("\nOpening browser for login..."));
3090
- console.log(chalk6.dim("Complete the login in your browser, then return here.\n"));
3091
- const loginUrl = `${WEB_APP_URL}/login?cli=true`;
3092
- await open(loginUrl);
3093
- const { token } = await inquirer.prompt([
3094
- {
3095
- type: "password",
3096
- name: "token",
3097
- message: "Paste your CLI token from the browser:",
3098
- mask: "*"
3099
- }
3100
- ]);
3101
- spinner.start("Verifying token...");
3102
- const supabase = getSupabaseClient();
3103
- const { data, error } = await supabase.auth.getUser(token);
3104
- if (error || !data.user) {
3105
- spinner.fail("Invalid token");
3106
- console.log(chalk6.red("The token is invalid or expired. Please try again."));
3107
- return;
3108
- }
3109
- const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
3110
- access_token: token,
3111
- refresh_token: ""
3112
- // Will need to handle this differently
3113
- });
3114
- if (sessionError || !sessionData.session) {
3115
- spinner.fail("Failed to establish session");
3116
- return;
3117
- }
3118
- saveSession(sessionData.session);
3119
- await fetchAndSaveSubscription();
3120
- spinner.succeed(`Logged in as ${chalk6.green(data.user.email)}`);
3121
- } else {
3122
- let email = options.email;
3123
- if (!email) {
3124
- const answers = await inquirer.prompt([
3125
- {
3126
- type: "input",
3127
- name: "email",
3128
- message: "Email:",
3129
- validate: (input) => {
3130
- if (!input.includes("@")) return "Please enter a valid email";
3131
- return true;
3132
- }
3133
- }
3134
- ]);
3135
- email = answers.email;
3136
- }
3137
- const { password } = await inquirer.prompt([
3138
- {
3139
- type: "password",
3140
- name: "password",
3141
- message: "Password:",
3142
- mask: "*"
3143
- }
3144
- ]);
3145
- spinner.start("Signing in...");
3146
- const supabase = getSupabaseClient();
3147
- const { data, error } = await supabase.auth.signInWithPassword({
3148
- email,
3149
- password
3150
- });
3151
- if (error) {
3152
- spinner.fail("Login failed");
3153
- console.log(chalk6.red(`
3154
- ${error.message}`));
3155
- if (error.message.includes("Invalid login")) {
3156
- console.log(chalk6.dim("\nDon't have an account? Sign up at:"));
3157
- console.log(chalk6.cyan(` ${WEB_APP_URL}/signup`));
3158
- }
3159
- return;
3160
- }
3161
- if (!data.session) {
3162
- spinner.fail("No session returned");
3163
- return;
3164
- }
3165
- saveSession(data.session);
3166
- await fetchAndSaveSubscription();
3167
- spinner.succeed(`Logged in as ${chalk6.green(data.user?.email)}`);
3168
- const { planName, sitesLimit } = getUserInfo();
3169
- console.log(chalk6.dim(`
3170
- Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
3171
- }
3172
- } catch (err) {
3173
- spinner.fail("Login failed");
3174
- console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
3175
- }
3176
- }
3177
- async function logout() {
3178
- if (!isLoggedIn()) {
3179
- console.log(chalk6.yellow("Not logged in"));
3180
- return;
3181
- }
3182
- const { email } = getUserInfo();
3183
- const spinner = ora5("Signing out...").start();
3184
- try {
3185
- const supabase = getSupabaseClient();
3186
- await supabase.auth.signOut();
3187
- clearCredentials();
3188
- spinner.succeed(`Logged out from ${chalk6.dim(email)}`);
3189
- } catch (err) {
3190
- spinner.fail("Logout failed");
3191
- console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
3192
- }
3193
- }
3194
- async function whoami() {
3195
- if (!isLoggedIn()) {
3196
- console.log(chalk6.yellow("Not logged in"));
3197
- console.log(chalk6.dim('\nRun "rankcli login" to sign in.'));
3198
- console.log(chalk6.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
3199
- return;
3200
- }
3201
- const spinner = ora5("Checking account...").start();
3202
- try {
3203
- if (isApiKeyAuth()) {
3204
- const apiKey = getApiKey();
3205
- if (apiKey) {
3206
- const result = await validateApiKey(apiKey);
3207
- if (result.valid && result.subscription) {
3208
- saveSubscription({
3209
- planId: result.subscription.planId,
3210
- planName: result.subscription.planName,
3211
- sitesLimit: result.subscription.sitesLimit
3212
- });
3213
- }
3214
- }
3215
- } else {
3216
- await fetchAndSaveSubscription();
3217
- }
3218
- const { email, planName, sitesLimit } = getUserInfo();
3219
- const authMethod = isApiKeyAuth() ? "API Key" : "Session";
3220
- spinner.stop();
3221
- console.log(chalk6.bold("\nRankCLI Account"));
3222
- console.log("\u2500".repeat(40));
3223
- console.log(` Email: ${chalk6.cyan(email || "N/A")}`);
3224
- console.log(` Plan: ${chalk6.green(planName)}`);
3225
- console.log(` Sites: ${sitesLimit} allowed`);
3226
- console.log(` Auth: ${chalk6.dim(authMethod)}`);
3227
- console.log("\u2500".repeat(40));
3228
- console.log(chalk6.dim(`
3229
- Manage at: ${WEB_APP_URL}/account`));
3230
- } catch (err) {
3231
- spinner.fail("Failed to fetch account info");
3232
- console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
3233
- }
3234
- }
3235
- async function fetchAndSaveSubscription() {
3236
- const supabase = await getAuthenticatedClient();
3237
- if (!supabase) return;
3238
- const { userId } = getUserInfo();
3239
- if (!userId) return;
3240
- const { data: subscription } = await supabase.from("user_subscriptions").select(`
3241
- plan:subscription_plans (
3242
- plan_id,
3243
- name,
3244
- sites_limit
3245
- )
3246
- `).eq("user_id", userId).single();
3247
- if (subscription?.plan) {
3248
- const plan = subscription.plan;
3249
- saveSubscription({
3250
- planId: plan.plan_id,
3251
- planName: plan.name,
3252
- sitesLimit: plan.sites_limit
3253
- });
3254
- } else {
3255
- saveSubscription({
3256
- planId: void 0,
3257
- planName: "Free",
3258
- sitesLimit: 1
3259
- });
3260
- }
3261
- }
3262
-
3263
3461
  // src/version.ts
3264
- var VERSION = "0.0.23";
3462
+ var VERSION = "0.0.25";
3265
3463
 
3266
3464
  // src/index.ts
3267
3465
  var program = new Command();
3268
3466
  program.name("rankcli").description("RankCLI - Ship code, get ranked. SEO meets CI/CD.").version(VERSION);
3269
- program.command("login").description("Log in to your RankCLI account").option("-e, --email <email>", "Email address").option("-b, --browser", "Open browser for login").option("-t, --token <apiKey>", "API key for non-interactive authentication").action(login);
3467
+ program.command("login").description("Log in to your RankCLI account (opens your browser by default)").option("-e, --email <email>", "Log in with email/password instead of the browser").option("--no-browser", "Skip the browser and log in with email/password interactively").option("-t, --token <apiKey>", "API key for non-interactive authentication").action(login);
3270
3468
  program.command("logout").description("Log out of your RankCLI account").action(logout);
3271
3469
  program.command("whoami").description("Show current logged in user").action(whoami);
3272
- program.command("init").description("Initialize SEO Autopilot in your project").option("-y, --yes", "Skip prompts and use defaults").action(init);
3470
+ program.command("init").description("Set up RankCLI in your project (detects framework, git/GitHub, AI keys)").option("-y, --yes", "Skip prompts and use defaults").action(init);
3273
3471
  program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
3274
- program.command("audit").description("Run a comprehensive SEO audit (200+ checks, Ahrefs-level)").option("-u, --url <url>", "URL to audit").option("-o, --output <format>", "Output format (json, console)", "console").option("--check-links", "Check for broken internal/external links (slower)").option("--max-pages <n>", "Max pages to crawl (default: 5)").option("--ai", "Enable AI analysis").option("--ai-provider <provider>", "AI provider: openai or anthropic").option("--ai-key <key>", "API key (or set OPENAI_API_KEY/ANTHROPIC_API_KEY)").action(audit);
3472
+ program.command("audit").description("Run a comprehensive SEO audit (200+ checks, Ahrefs-level)").option("-u, --url <url>", "URL to audit").option("-o, --output <format>", "Output format (json, console)", "console").option("--check-links", "Check for broken internal/external links (slower)").option("--max-pages <n>", "Max pages to crawl (default: 5)").option("--ai", "Enable AI analysis").option("--ai-provider <provider>", "AI provider: openai, anthropic, openrouter, groq, or gemini (auto-detected from env if omitted)").option("--ai-key <key>", "API key (or set OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, GROQ_API_KEY, or GEMINI_API_KEY)").action(audit);
3275
3473
  program.command("apply").description("Analyze a live URL and apply SEO fixes to your codebase").option("-u, --url <url>", "URL to analyze").option("--auto", "Auto-apply fixes without confirmation").option("--dry-run", "Preview changes without applying").action(apply);
3276
3474
  program.command("fix").description("Agentic SEO fixer - detect, fix, commit, and push").option("-u, --url <url>", "URL to analyze").option("--auto", "Auto-apply fixes without confirmation").option("--commit", "Commit changes with conventional commit message").option("--push", "Push changes after committing (requires --commit)").option("--dry-run", "Preview changes without applying").option("--only <codes>", "Fix only specific issue codes (comma-separated)", (val) => val.split(",")).option("-o, --output <format>", "Output format (text, json)", "text").action(async (options) => {
3277
3475
  const result = await fix({