@rankcli/cli 0.0.18 → 0.0.20

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 (4) hide show
  1. package/README.md +17 -12
  2. package/dist/index.js +1417 -210
  3. package/dist/index.mjs +1379 -168
  4. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -284,7 +284,7 @@ async function analyze(options) {
284
284
  // src/commands/audit.ts
285
285
  var import_agent_runtime = require("@rankcli/agent-runtime");
286
286
  var import_ora = __toESM(require("ora"));
287
- var import_chalk = __toESM(require("chalk"));
287
+ var import_chalk2 = __toESM(require("chalk"));
288
288
 
289
289
  // src/lib/config.ts
290
290
  var import_conf = __toESM(require("conf"));
@@ -479,6 +479,191 @@ async function getAuthenticatedClient() {
479
479
  return supabase;
480
480
  }
481
481
 
482
+ // src/lib/upsell.ts
483
+ var import_chalk = __toESM(require("chalk"));
484
+ var WEB_APP_URL2 = "https://rankcli.dev";
485
+ var TIER_LIMITS = {
486
+ free_anon: {
487
+ checks: 50,
488
+ sites: 0,
489
+ domains_per_month: 1,
490
+ features: { geo: false, auto_fix: false, github: false, ai: false }
491
+ },
492
+ free: {
493
+ checks: 100,
494
+ sites: 1,
495
+ domains_per_month: 2,
496
+ features: { geo: "basic", auto_fix: false, github: false, ai: false }
497
+ },
498
+ solo: {
499
+ checks: 280,
500
+ sites: 3,
501
+ domains_per_month: 5,
502
+ features: { geo: true, auto_fix: true, github: false, ai: true }
503
+ },
504
+ pro: {
505
+ checks: 280,
506
+ sites: 10,
507
+ domains_per_month: 15,
508
+ features: { geo: true, auto_fix: true, github: true, ai: true }
509
+ },
510
+ agency: {
511
+ checks: 280,
512
+ sites: 50,
513
+ domains_per_month: null,
514
+ // unlimited
515
+ features: { geo: true, auto_fix: true, github: true, ai: true }
516
+ }
517
+ };
518
+ var UPSELL_MESSAGES = {
519
+ // After free audit completes
520
+ free_audit_complete: () => [
521
+ "",
522
+ import_chalk.default.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"),
523
+ import_chalk.default.yellow.bold(" \u{1F680} Get 2x more checks + audit history"),
524
+ import_chalk.default.dim(" Create a free account to unlock 100 checks and save your results."),
525
+ "",
526
+ import_chalk.default.cyan(` \u2192 rankcli login`),
527
+ import_chalk.default.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"),
528
+ ""
529
+ ],
530
+ // When checks limit is reached
531
+ checks_limit: (ctx) => [
532
+ "",
533
+ import_chalk.default.yellow(`\u26A0\uFE0F Running ${ctx.current || 50} of ${TIER_LIMITS.solo.checks} available checks`),
534
+ import_chalk.default.dim(` Unlock all ${TIER_LIMITS.solo.checks}+ checks with Solo ($9/mo)`),
535
+ import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_checks`),
536
+ ""
537
+ ],
538
+ // When domain limit is reached
539
+ domain_limit: (ctx) => [
540
+ "",
541
+ import_chalk.default.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"),
542
+ import_chalk.default.red.bold(" \u2717 Domain limit reached"),
543
+ import_chalk.default.dim(` Your ${ctx.tier} plan allows ${ctx.max} domains per month.`),
544
+ "",
545
+ import_chalk.default.dim(" Upgrade to audit more sites:"),
546
+ import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_domain_limit`),
547
+ import_chalk.default.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"),
548
+ ""
549
+ ],
550
+ // When GEO features are limited
551
+ geo_limited: (ctx) => [
552
+ "",
553
+ import_chalk.default.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"),
554
+ import_chalk.default.yellow.bold(" \u{1F916} Full GEO analysis requires Solo"),
555
+ import_chalk.default.dim(" You're seeing basic AI crawler access checks."),
556
+ import_chalk.default.dim(" Full analysis includes: LLM signals, citation readiness, content structure"),
557
+ "",
558
+ import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_geo`),
559
+ import_chalk.default.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"),
560
+ ""
561
+ ],
562
+ // When auto-fix is attempted without access
563
+ auto_fix_blocked: () => [
564
+ "",
565
+ import_chalk.default.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"),
566
+ import_chalk.default.red.bold(" \u2717 Auto-fix requires Solo plan"),
567
+ import_chalk.default.dim(" Apply framework-specific SEO fixes directly to your codebase."),
568
+ "",
569
+ import_chalk.default.dim(" Solo ($9/mo) includes:"),
570
+ import_chalk.default.dim(" \u2022 Local auto-fix via CLI"),
571
+ import_chalk.default.dim(" \u2022 25+ framework support"),
572
+ import_chalk.default.dim(" \u2022 Full 280+ checks"),
573
+ "",
574
+ import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_autofix`),
575
+ import_chalk.default.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"),
576
+ ""
577
+ ],
578
+ // When GitHub integration is attempted without access
579
+ github_blocked: () => [
580
+ "",
581
+ import_chalk.default.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"),
582
+ import_chalk.default.red.bold(" \u2717 GitHub auto-fix PRs require Pro"),
583
+ import_chalk.default.dim(" Automatically create fix PRs when SEO issues are detected."),
584
+ "",
585
+ import_chalk.default.dim(" Pro ($29/mo) includes:"),
586
+ import_chalk.default.dim(" \u2022 GitHub App integration"),
587
+ import_chalk.default.dim(" \u2022 Auto-fix PRs"),
588
+ import_chalk.default.dim(" \u2022 Daily scheduled audits"),
589
+ import_chalk.default.dim(" \u2022 Slack/Discord alerts"),
590
+ "",
591
+ import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_github`),
592
+ import_chalk.default.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"),
593
+ ""
594
+ ],
595
+ // Keyword tracking limit reached
596
+ keyword_limit: (ctx) => [
597
+ "",
598
+ import_chalk.default.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"),
599
+ import_chalk.default.red.bold(" \u2717 Keyword tracking limit reached"),
600
+ import_chalk.default.dim(` Your ${ctx.tier} plan allows ${ctx.limit || ctx.max} tracked keywords.`),
601
+ import_chalk.default.dim(` Currently tracking: ${ctx.current}`),
602
+ "",
603
+ import_chalk.default.dim(" Upgrade for more keywords:"),
604
+ import_chalk.default.dim(" \u2022 Solo ($9/mo): 100 keywords"),
605
+ import_chalk.default.dim(" \u2022 Pro ($29/mo): 500 keywords"),
606
+ import_chalk.default.dim(" \u2022 Agency ($99/mo): 2,000 keywords"),
607
+ "",
608
+ import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_keyword_limit`),
609
+ import_chalk.default.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"),
610
+ ""
611
+ ],
612
+ // Rate limit exceeded
613
+ rate_limit: (ctx) => [
614
+ "",
615
+ import_chalk.default.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"),
616
+ import_chalk.default.red.bold(" \u2717 API rate limit exceeded"),
617
+ import_chalk.default.dim(` Your ${ctx.tier} plan allows ${ctx.max} requests/day.`),
618
+ "",
619
+ import_chalk.default.dim(" Need more? Upgrade for higher limits:"),
620
+ import_chalk.default.dim(" \u2022 Solo: 100 req/day"),
621
+ import_chalk.default.dim(" \u2022 Pro: 1,000 req/day"),
622
+ import_chalk.default.dim(" \u2022 Agency: 10,000 req/day"),
623
+ "",
624
+ import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_rate_limit`),
625
+ import_chalk.default.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"),
626
+ ""
627
+ ],
628
+ // Schedule exceeded (for GitHub Action)
629
+ schedule_exceeded: (ctx) => [
630
+ "",
631
+ import_chalk.default.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"),
632
+ import_chalk.default.yellow.bold(" \u23F0 Scheduled audit skipped"),
633
+ import_chalk.default.dim(` Your ${ctx.tier} plan allows ${ctx.limit} scheduled audits.`),
634
+ "",
635
+ import_chalk.default.dim(" Upgrade for more frequent audits:"),
636
+ import_chalk.default.dim(" \u2022 Solo: Weekly"),
637
+ import_chalk.default.dim(" \u2022 Pro: Daily"),
638
+ import_chalk.default.dim(" \u2022 Agency: Hourly"),
639
+ "",
640
+ import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=schedule_exceeded`),
641
+ import_chalk.default.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"),
642
+ ""
643
+ ],
644
+ // Login prompt (soft)
645
+ login_prompt: () => [
646
+ "",
647
+ import_chalk.default.dim(" \u{1F4A1} Tip: Log in to sync results and track progress over time:"),
648
+ import_chalk.default.dim(" rankcli login"),
649
+ ""
650
+ ]
651
+ };
652
+ function showUpsell(messageKey, context) {
653
+ const messageFn = UPSELL_MESSAGES[messageKey];
654
+ if (!messageFn) return;
655
+ const ctx = {
656
+ tier: "free",
657
+ ...context
658
+ };
659
+ const lines = messageFn(ctx);
660
+ lines.forEach((line) => console.log(line));
661
+ }
662
+ function hasFeature(tier, feature) {
663
+ const limits = TIER_LIMITS[tier];
664
+ return limits?.features?.[feature] ?? false;
665
+ }
666
+
482
667
  // src/commands/audit.ts
483
668
  var { detectFramework: detectFramework2, getFrameworkDisplayName, getFrameworkIssues } = import_agent_runtime.frameworks;
484
669
  async function fetchHtml(url, timeout = 1e4) {
@@ -546,7 +731,7 @@ Return JSON with:
546
731
  }
547
732
  const response = await fetch(apiUrl, { method: "POST", headers, body });
548
733
  if (!response.ok) {
549
- console.error(import_chalk.default.yellow(`AI analysis failed: ${response.status}`));
734
+ console.error(import_chalk2.default.yellow(`AI analysis failed: ${response.status}`));
550
735
  return null;
551
736
  }
552
737
  const data = await response.json();
@@ -556,7 +741,7 @@ Return JSON with:
556
741
  if (!jsonMatch) return null;
557
742
  return JSON.parse(jsonMatch[0]);
558
743
  } catch (error) {
559
- console.error(import_chalk.default.yellow(`AI analysis error: ${error}`));
744
+ console.error(import_chalk2.default.yellow(`AI analysis error: ${error}`));
560
745
  return null;
561
746
  }
562
747
  }
@@ -575,7 +760,7 @@ function mergeIssues(issueArrays) {
575
760
  }
576
761
  return Array.from(issueMap.values());
577
762
  }
578
- async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis) {
763
+ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis, durationMs, checksRun) {
579
764
  const supabase = await getAuthenticatedClient();
580
765
  if (!supabase) return null;
581
766
  const { userId, sitesLimit } = getUserInfo();
@@ -591,9 +776,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
591
776
  const { count } = await supabase.from("projects").select("id", { count: "exact", head: true }).eq("user_id", userId).eq("is_active", true);
592
777
  const limit = sitesLimit ?? 1;
593
778
  if (count !== null && count >= limit) {
594
- console.log(import_chalk.default.yellow(`
779
+ console.log(import_chalk2.default.yellow(`
595
780
  \u26A0\uFE0F Project limit reached (${count}/${limit})`));
596
- console.log(import_chalk.default.dim(` Upgrade at: ${WEB_APP_URL}/account`));
781
+ console.log(import_chalk2.default.dim(` Upgrade at: ${WEB_APP_URL}/account`));
597
782
  return null;
598
783
  }
599
784
  const { data: newProject, error: projectError } = await supabase.from("projects").insert({
@@ -603,7 +788,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
603
788
  domain
604
789
  }).select("id").single();
605
790
  if (projectError || !newProject) {
606
- console.error(import_chalk.default.dim("Failed to create project"));
791
+ console.error(import_chalk2.default.dim("Failed to create project"));
607
792
  return null;
608
793
  }
609
794
  projectId = newProject.id;
@@ -615,8 +800,11 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
615
800
  onPage: report.healthScore?.onPage || 0,
616
801
  content: report.healthScore?.content || 0,
617
802
  performance: report.healthScore?.performance || 0,
618
- security: report.healthScore?.security || 0
803
+ security: report.healthScore?.security || 0,
804
+ localSeo: report.healthScore?.localSeo || 0,
805
+ aiReadiness: report.healthScore?.aiReadiness || 0
619
806
  };
807
+ const now = (/* @__PURE__ */ new Date()).toISOString();
620
808
  const { data: audit2, error: auditError } = await supabase.from("audits").insert({
621
809
  project_id: projectId,
622
810
  triggered_by: "cli",
@@ -625,21 +813,23 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
625
813
  score,
626
814
  pages_crawled: pagesAudited,
627
815
  issues_found: mergedIssues.length,
628
- checks_run: mergedIssues.length > 0 ? 200 : 0,
629
- // Approximate
816
+ checks_run: checksRun,
630
817
  health_scores: healthScores,
631
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
818
+ started_at: new Date(Date.now() - durationMs).toISOString(),
819
+ completed_at: now,
820
+ duration_ms: durationMs,
632
821
  summary: {
633
822
  errors: mergedIssues.filter((i) => i.severity === "error").length,
634
823
  warnings: mergedIssues.filter((i) => i.severity === "warning").length,
635
824
  notices: mergedIssues.filter((i) => i.severity === "notice").length,
636
825
  pagesAudited,
826
+ checksRun,
637
827
  source: "cli"
638
828
  },
639
829
  ai_analysis: aiAnalysis
640
830
  }).select("id").single();
641
831
  if (auditError || !audit2) {
642
- console.error(import_chalk.default.dim("Failed to save audit"));
832
+ console.error(import_chalk2.default.dim("Failed to save audit"));
643
833
  return null;
644
834
  }
645
835
  if (mergedIssues.length > 0) {
@@ -660,12 +850,14 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
660
850
  await supabase.from("audit_issues").insert(issueRecords);
661
851
  }
662
852
  await supabase.from("projects").update({
663
- last_audit_at: (/* @__PURE__ */ new Date()).toISOString(),
664
- last_audit_score: score
853
+ last_audit_at: now,
854
+ last_audit_score: score,
855
+ last_scheduled_audit_at: now,
856
+ scheduled_audit_source: "cli"
665
857
  }).eq("id", projectId);
666
858
  return { projectId, auditId: audit2.id };
667
859
  } catch (error) {
668
- console.error(import_chalk.default.dim("Cloud sync failed"));
860
+ console.error(import_chalk2.default.dim("Cloud sync failed"));
669
861
  return null;
670
862
  }
671
863
  }
@@ -674,7 +866,7 @@ async function audit(options) {
674
866
  const maxPages = parseInt(options.maxPages || "5", 10);
675
867
  const enableAI = options.ai || false;
676
868
  const aiProvider = options.aiProvider || "openai";
677
- const aiKey = options.aiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || "";
869
+ 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) || "";
678
870
  const loggedIn = isLoggedIn();
679
871
  if (!url) {
680
872
  console.log("\n\u26A0\uFE0F Please provide a URL to audit:");
@@ -686,8 +878,8 @@ async function audit(options) {
686
878
  console.log(" --ai-key <key> API key (or set OPENAI_API_KEY/ANTHROPIC_API_KEY)");
687
879
  console.log(" --output json Output as JSON");
688
880
  if (!loggedIn) {
689
- console.log(import_chalk.default.dim("\nTip: Log in to sync results to the web dashboard:"));
690
- console.log(import_chalk.default.dim(" rankcli login"));
881
+ console.log(import_chalk2.default.dim("\nTip: Log in to sync results to the web dashboard:"));
882
+ console.log(import_chalk2.default.dim(" rankcli login"));
691
883
  }
692
884
  console.log("");
693
885
  return;
@@ -771,12 +963,17 @@ async function audit(options) {
771
963
  aiSpinner.warn("AI analysis failed or returned no results");
772
964
  }
773
965
  } else if (enableAI && !aiKey) {
774
- console.log(import_chalk.default.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
775
- console.log(import_chalk.default.dim(" Set OPENAI_API_KEY or ANTHROPIC_API_KEY, or use --ai-key"));
966
+ console.log(import_chalk2.default.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
967
+ console.log(import_chalk2.default.dim(" Set OPENAI_API_KEY or ANTHROPIC_API_KEY, or use --ai-key"));
776
968
  }
777
969
  const totalTime = Date.now() - startTime;
778
970
  const frameworkName = detectedFramework.framework !== "unknown" ? getFrameworkDisplayName(detectedFramework.framework) : null;
971
+ const actualChecksRun = (primaryReport.summary?.passed || 0) + (primaryReport.summary?.errors || 0) + (primaryReport.summary?.warnings || 0) || 200;
779
972
  if (options.output === "json") {
973
+ let syncResult = null;
974
+ if (loggedIn) {
975
+ syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
976
+ }
780
977
  const jsonOutput = {
781
978
  url,
782
979
  score: primaryReport.healthScore?.overall || 0,
@@ -785,85 +982,106 @@ async function audit(options) {
785
982
  framework: frameworkName,
786
983
  issues: mergedIssues,
787
984
  healthScores: primaryReport.healthScore,
788
- aiAnalysis
985
+ aiAnalysis,
986
+ // Include cloud sync info if synced
987
+ ...syncResult && {
988
+ projectId: syncResult.projectId,
989
+ auditId: syncResult.auditId,
990
+ dashboardUrl: `${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}`
991
+ }
789
992
  };
790
993
  console.log(JSON.stringify(jsonOutput, null, 2));
791
994
  } else {
792
- console.log("\n" + import_chalk.default.bold("\u2550".repeat(60)));
793
- console.log(import_chalk.default.bold.cyan(` SEO AUDIT REPORT: ${url}`));
794
- console.log(import_chalk.default.bold("\u2550".repeat(60)));
995
+ console.log("\n" + import_chalk2.default.bold("\u2550".repeat(60)));
996
+ console.log(import_chalk2.default.bold.cyan(` SEO AUDIT REPORT: ${url}`));
997
+ console.log(import_chalk2.default.bold("\u2550".repeat(60)));
795
998
  const score = primaryReport.healthScore?.overall || 0;
796
- const scoreColor = score >= 80 ? import_chalk.default.green : score >= 60 ? import_chalk.default.yellow : import_chalk.default.red;
999
+ const scoreColor = score >= 80 ? import_chalk2.default.green : score >= 60 ? import_chalk2.default.yellow : import_chalk2.default.red;
797
1000
  console.log(`
798
- ${import_chalk.default.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
799
- console.log(` ${import_chalk.default.bold("Pages audited:")} ${successCount}`);
800
- console.log(` ${import_chalk.default.bold("Issues found:")} ${mergedIssues.length}`);
801
- console.log(` ${import_chalk.default.bold("Time:")} ${totalTime}ms`);
1001
+ ${import_chalk2.default.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
1002
+ console.log(` ${import_chalk2.default.bold("Pages audited:")} ${successCount}`);
1003
+ console.log(` ${import_chalk2.default.bold("Issues found:")} ${mergedIssues.length}`);
1004
+ console.log(` ${import_chalk2.default.bold("Time:")} ${totalTime}ms`);
802
1005
  if (frameworkName) {
803
- console.log(` ${import_chalk.default.bold("Framework:")} ${import_chalk.default.cyan(frameworkName)}`);
1006
+ console.log(` ${import_chalk2.default.bold("Framework:")} ${import_chalk2.default.cyan(frameworkName)}`);
804
1007
  }
805
1008
  if (primaryReport.healthScore) {
806
- console.log("\n" + import_chalk.default.bold(" Health Scores:"));
1009
+ console.log("\n" + import_chalk2.default.bold(" Health Scores:"));
807
1010
  const hs = primaryReport.healthScore;
808
1011
  const bar = (score2) => {
809
1012
  const filled = Math.round(score2 / 5);
810
1013
  const empty = 20 - filled;
811
- const color = score2 >= 80 ? import_chalk.default.green : score2 >= 60 ? import_chalk.default.yellow : import_chalk.default.red;
812
- return color("\u2588".repeat(filled)) + import_chalk.default.gray("\u2591".repeat(empty));
1014
+ const color = score2 >= 80 ? import_chalk2.default.green : score2 >= 60 ? import_chalk2.default.yellow : import_chalk2.default.red;
1015
+ return color("\u2588".repeat(filled)) + import_chalk2.default.gray("\u2591".repeat(empty));
813
1016
  };
814
1017
  console.log(` Crawlability: ${bar(hs.crawlability || 0)} ${hs.crawlability || 0}%`);
815
1018
  console.log(` On-Page: ${bar(hs.onPage || 0)} ${hs.onPage || 0}%`);
816
1019
  console.log(` Content: ${bar(hs.content || 0)} ${hs.content || 0}%`);
817
1020
  console.log(` Performance: ${bar(hs.performance || 0)} ${hs.performance || 0}%`);
818
1021
  console.log(` Security: ${bar(hs.security || 0)} ${hs.security || 0}%`);
1022
+ console.log(` Local SEO: ${bar(hs.localSeo || 0)} ${hs.localSeo || 0}%`);
1023
+ console.log(` AI Readiness: ${bar(hs.aiReadiness || 0)} ${hs.aiReadiness || 0}%`);
819
1024
  }
820
1025
  const errors = mergedIssues.filter((i) => i.severity === "error");
821
1026
  const warnings = mergedIssues.filter((i) => i.severity === "warning");
822
1027
  const notices = mergedIssues.filter((i) => i.severity === "notice");
823
- console.log("\n" + import_chalk.default.bold(" Issues by Severity:"));
824
- console.log(` ${import_chalk.default.red("\u25CF")} Errors: ${errors.length}`);
825
- console.log(` ${import_chalk.default.yellow("\u25CF")} Warnings: ${warnings.length}`);
826
- console.log(` ${import_chalk.default.blue("\u25CF")} Notices: ${notices.length}`);
827
- console.log("\n" + import_chalk.default.bold(" Top Issues:"));
1028
+ console.log("\n" + import_chalk2.default.bold(" Issues by Severity:"));
1029
+ console.log(` ${import_chalk2.default.red("\u25CF")} Errors: ${errors.length}`);
1030
+ console.log(` ${import_chalk2.default.yellow("\u25CF")} Warnings: ${warnings.length}`);
1031
+ console.log(` ${import_chalk2.default.blue("\u25CF")} Notices: ${notices.length}`);
1032
+ console.log("\n" + import_chalk2.default.bold(" Top Issues:"));
828
1033
  const topIssues = [...errors, ...warnings, ...notices].slice(0, 10);
829
1034
  topIssues.forEach((issue, i) => {
830
- const icon = issue.severity === "error" ? import_chalk.default.red("\u2717") : issue.severity === "warning" ? import_chalk.default.yellow("!") : import_chalk.default.blue("i");
1035
+ const icon = issue.severity === "error" ? import_chalk2.default.red("\u2717") : issue.severity === "warning" ? import_chalk2.default.yellow("!") : import_chalk2.default.blue("i");
831
1036
  console.log(` ${i + 1}. ${icon} ${issue.title}`);
832
1037
  if (issue.affectedUrls && issue.affectedUrls.length > 1) {
833
- console.log(import_chalk.default.dim(` Affects ${issue.affectedUrls.length} pages`));
1038
+ console.log(import_chalk2.default.dim(` Affects ${issue.affectedUrls.length} pages`));
834
1039
  }
835
1040
  });
836
1041
  if (aiAnalysis) {
837
- console.log("\n" + import_chalk.default.bold(" AI Analysis:"));
1042
+ console.log("\n" + import_chalk2.default.bold(" AI Analysis:"));
838
1043
  console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
839
- console.log("\n " + import_chalk.default.bold("Suggestions:"));
1044
+ console.log("\n " + import_chalk2.default.bold("Suggestions:"));
840
1045
  aiAnalysis.suggestions.forEach((s, i) => {
841
1046
  console.log(` ${i + 1}. ${s}`);
842
1047
  });
843
1048
  if (aiAnalysis.prioritizedFixes?.length > 0) {
844
- console.log("\n " + import_chalk.default.bold("Priority Fixes:"));
845
- aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix, i) => {
846
- console.log(` ${i + 1}. [P${fix.priority}] ${fix.code}`);
847
- console.log(import_chalk.default.dim(` ${fix.rationale}`));
1049
+ console.log("\n " + import_chalk2.default.bold("Priority Fixes:"));
1050
+ aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix2, i) => {
1051
+ console.log(` ${i + 1}. [P${fix2.priority}] ${fix2.code}`);
1052
+ console.log(import_chalk2.default.dim(` ${fix2.rationale}`));
848
1053
  });
849
1054
  }
850
1055
  }
851
- console.log("\n" + import_chalk.default.bold("\u2550".repeat(60)));
1056
+ console.log("\n" + import_chalk2.default.bold("\u2550".repeat(60)));
1057
+ const userInfo = getUserInfo();
1058
+ const tier = userInfo.planName?.toLowerCase() || "free";
1059
+ const isFreeTier = !loggedIn || tier === "free";
1060
+ const isSoloOrBelow = isFreeTier || tier === "solo";
1061
+ if (isFreeTier) {
1062
+ const checksRun = mergedIssues.length > 0 ? loggedIn ? 100 : 50 : 0;
1063
+ showUpsell("checks_limit", {
1064
+ tier: loggedIn ? "free" : "free_anon",
1065
+ current: checksRun
1066
+ });
1067
+ }
1068
+ if (options.geo && isFreeTier) {
1069
+ showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
1070
+ }
852
1071
  if (loggedIn) {
853
1072
  const syncSpinner = (0, import_ora.default)("Syncing to cloud...").start();
854
- const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis);
1073
+ const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
855
1074
  if (syncResult) {
856
1075
  syncSpinner.succeed("Results synced to cloud");
857
- console.log(import_chalk.default.cyan(`
1076
+ console.log(import_chalk2.default.cyan(`
858
1077
  View and manage issues in the web dashboard:`));
859
- console.log(import_chalk.default.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
1078
+ console.log(import_chalk2.default.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
860
1079
  `));
861
1080
  } else {
862
1081
  syncSpinner.warn("Could not sync to cloud");
863
1082
  }
864
1083
  } else {
865
- console.log(import_chalk.default.dim("\n Tip: Log in to sync results and manage issues in the web dashboard:"));
866
- console.log(import_chalk.default.dim(" rankcli login\n"));
1084
+ showUpsell("free_audit_complete");
867
1085
  }
868
1086
  }
869
1087
  } catch (error) {
@@ -882,19 +1100,19 @@ var SEVERITY_ICON = {
882
1100
  warning: "\u26A0\uFE0F",
883
1101
  info: "\u2139\uFE0F"
884
1102
  };
885
- function printFix(fix, index) {
886
- const icon = SEVERITY_ICON[fix.issue.severity] || "\u2022";
1103
+ function printFix(fix2, index) {
1104
+ const icon = SEVERITY_ICON[fix2.issue.severity] || "\u2022";
887
1105
  console.log(`
888
- ${index + 1}. ${icon} ${fix.issue.message}`);
889
- console.log(` File: ${fix.file}`);
890
- if (fix.skipped) {
891
- console.log(` Status: SKIPPED - ${fix.skipReason}`);
1106
+ ${index + 1}. ${icon} ${fix2.issue.message}`);
1107
+ console.log(` File: ${fix2.file}`);
1108
+ if (fix2.skipped) {
1109
+ console.log(` Status: SKIPPED - ${fix2.skipReason}`);
892
1110
  } else {
893
- console.log(` Explanation: ${fix.explanation}`);
894
- if (fix.before) {
895
- console.log(` Before: ${truncate(fix.before, 60)}`);
1111
+ console.log(` Explanation: ${fix2.explanation}`);
1112
+ if (fix2.before) {
1113
+ console.log(` Before: ${truncate(fix2.before, 60)}`);
896
1114
  }
897
- console.log(` After: ${truncate(fix.after, 100)}`);
1115
+ console.log(` After: ${truncate(fix2.after, 100)}`);
898
1116
  }
899
1117
  }
900
1118
  function truncate(str, maxLen) {
@@ -930,40 +1148,37 @@ async function apply(options) {
930
1148
  }
931
1149
  console.log("\n\u{1F680} SEO Autopilot - Apply Fixes");
932
1150
  console.log("\u2550".repeat(50));
1151
+ const loggedIn = isLoggedIn();
1152
+ const userInfo = getUserInfo();
1153
+ const tier = userInfo.planName?.toLowerCase() || "free";
1154
+ const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
1155
+ if (!hasAutoFix) {
1156
+ showUpsell("auto_fix_blocked");
1157
+ return;
1158
+ }
933
1159
  console.log("\n\u{1F50D} Detecting framework...");
934
1160
  const frameworkResult = await (0, import_agent_runtime2.detectFramework)({ cwd });
935
1161
  const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
936
1162
  console.log(` Framework: ${framework.name}`);
937
- const isPaidUser = isLoggedIn();
938
1163
  console.log(`
939
1164
  \u{1F50D} Analyzing ${url}...`);
940
- if (isPaidUser) {
941
- console.log(" Running full audit (200+ checks)...");
942
- } else {
943
- console.log(" Running basic analysis (login for full 200+ checks)");
944
- }
1165
+ console.log(" Running full audit (200+ checks)...");
945
1166
  let issues;
946
1167
  let score;
947
1168
  let auditResult = null;
948
1169
  try {
949
- if (isPaidUser) {
950
- auditResult = await (0, import_agent_runtime2.runAuditWithFixes)({
951
- url,
952
- generateFixes: true
953
- });
954
- issues = auditResult.report.issues.map((issue) => ({
955
- severity: issue.severity,
956
- category: issue.category,
957
- code: issue.code,
958
- message: issue.title,
959
- impact: issue.description || void 0
960
- }));
961
- score = auditResult.score;
962
- } else {
963
- const analysis = await (0, import_agent_runtime2.runDirectAnalysis)(url);
964
- issues = analysis.issues;
965
- score = analysis.score;
966
- }
1170
+ auditResult = await (0, import_agent_runtime2.runAuditWithFixes)({
1171
+ url,
1172
+ generateFixes: true
1173
+ });
1174
+ issues = auditResult.report.issues.map((issue) => ({
1175
+ severity: issue.severity,
1176
+ category: issue.category,
1177
+ code: issue.code,
1178
+ message: issue.title,
1179
+ impact: issue.description || void 0
1180
+ }));
1181
+ score = auditResult.score;
967
1182
  } catch (error) {
968
1183
  console.log(`
969
1184
  \u274C Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}
@@ -1023,15 +1238,15 @@ async function apply(options) {
1023
1238
  if (applied.length > 0) {
1024
1239
  console.log(`
1025
1240
  \u2705 Applied ${applied.length} fix(es):`);
1026
- for (const fix of applied) {
1027
- console.log(` - ${fix.file}: ${fix.issue.message}`);
1241
+ for (const fix2 of applied) {
1242
+ console.log(` - ${fix2.file}: ${fix2.issue.message}`);
1028
1243
  }
1029
1244
  }
1030
1245
  if (skipped.length > 0) {
1031
1246
  console.log(`
1032
1247
  \u26A0\uFE0F Skipped ${skipped.length} fix(es):`);
1033
- for (const fix of skipped) {
1034
- console.log(` - ${fix.file}: ${fix.skipReason || "Manual fix required"}`);
1248
+ for (const fix2 of skipped) {
1249
+ console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
1035
1250
  }
1036
1251
  }
1037
1252
  if (config2 && !config2.url) {
@@ -1051,10 +1266,788 @@ async function apply(options) {
1051
1266
  console.log("");
1052
1267
  }
1053
1268
 
1054
- // src/commands/geo.ts
1269
+ // src/commands/fix.ts
1270
+ var import_child_process = require("child_process");
1271
+ var import_util = require("util");
1055
1272
  var import_agent_runtime3 = require("@rankcli/agent-runtime");
1056
1273
  var import_ora2 = __toESM(require("ora"));
1057
- var import_chalk2 = __toESM(require("chalk"));
1274
+ var import_chalk3 = __toESM(require("chalk"));
1275
+ var execAsync = (0, import_util.promisify)(import_child_process.exec);
1276
+ var SEVERITY_ICON2 = {
1277
+ critical: "\u274C",
1278
+ error: "\u274C",
1279
+ warning: "\u26A0\uFE0F",
1280
+ notice: "\u2139\uFE0F",
1281
+ info: "\u2139\uFE0F"
1282
+ };
1283
+ function generateCommitMessage(fixes, url) {
1284
+ const applied = fixes.filter((f) => f.applied && !f.skipped);
1285
+ if (applied.length === 0) {
1286
+ return "";
1287
+ }
1288
+ const isAddingNew = applied.some(
1289
+ (f) => f.issue.code.includes("MISSING") || f.issue.message.toLowerCase().includes("missing") || f.issue.message.toLowerCase().includes("add")
1290
+ );
1291
+ const type = isAddingNew ? "feat" : "fix";
1292
+ const categories = new Set(applied.map((f) => f.issue.category || "seo"));
1293
+ const categoryStr = categories.size === 1 ? Array.from(categories)[0] : "seo";
1294
+ const issueCount = applied.length;
1295
+ const title = `${type}(${categoryStr}): fix ${issueCount} SEO issue${issueCount > 1 ? "s" : ""}`;
1296
+ const fixList = applied.map((f) => `- ${f.issue.code}: ${f.issue.message}`).join("\n");
1297
+ const body = `Fixed:
1298
+ ${fixList}
1299
+
1300
+ URL: ${url}
1301
+ Generated by: rankcli fix`;
1302
+ return `${title}
1303
+
1304
+ ${body}`;
1305
+ }
1306
+ async function executeGitCommit(cwd, commitMessage, push) {
1307
+ try {
1308
+ await execAsync("git add -A", { cwd });
1309
+ const escapedMessage = commitMessage.replace(/"/g, '\\"');
1310
+ await execAsync(`git commit -m "${escapedMessage}"`, { cwd });
1311
+ let pushed = false;
1312
+ if (push) {
1313
+ try {
1314
+ await execAsync("git push", { cwd });
1315
+ pushed = true;
1316
+ } catch (pushErr) {
1317
+ return {
1318
+ committed: true,
1319
+ pushed: false,
1320
+ error: `Push failed: ${pushErr instanceof Error ? pushErr.message : "Unknown error"}`
1321
+ };
1322
+ }
1323
+ }
1324
+ return { committed: true, pushed };
1325
+ } catch (err) {
1326
+ return {
1327
+ committed: false,
1328
+ pushed: false,
1329
+ error: err instanceof Error ? err.message : "Git operation failed"
1330
+ };
1331
+ }
1332
+ }
1333
+ function truncate2(str, maxLen) {
1334
+ const oneLine = str.replace(/\n/g, " ").trim();
1335
+ if (oneLine.length <= maxLen) return oneLine;
1336
+ return oneLine.substring(0, maxLen - 3) + "...";
1337
+ }
1338
+ function printFix2(fix2, index) {
1339
+ const icon = SEVERITY_ICON2[fix2.issue.severity] || "\u2022";
1340
+ console.log(`
1341
+ ${index + 1}. ${icon} ${fix2.issue.message}`);
1342
+ console.log(` Code: ${import_chalk3.default.dim(fix2.issue.code)}`);
1343
+ console.log(` File: ${import_chalk3.default.cyan(fix2.file)}`);
1344
+ if (fix2.skipped) {
1345
+ console.log(` Status: ${import_chalk3.default.yellow("SKIPPED")} - ${fix2.skipReason}`);
1346
+ } else if (fix2.applied) {
1347
+ console.log(` Status: ${import_chalk3.default.green("APPLIED")}`);
1348
+ console.log(` ${import_chalk3.default.dim("Explanation:")} ${fix2.explanation}`);
1349
+ } else {
1350
+ console.log(` ${import_chalk3.default.dim("Explanation:")} ${fix2.explanation}`);
1351
+ if (fix2.before) {
1352
+ console.log(` ${import_chalk3.default.red("Before:")} ${truncate2(fix2.before, 60)}`);
1353
+ }
1354
+ console.log(` ${import_chalk3.default.green("After:")} ${truncate2(fix2.after, 100)}`);
1355
+ }
1356
+ }
1357
+ async function fix(options) {
1358
+ const startTime = Date.now();
1359
+ const cwd = options.cwd || process.cwd();
1360
+ const url = options.url;
1361
+ if (!url) {
1362
+ const error = "Please provide a URL to fix:\n rankcli fix --url https://your-site.com";
1363
+ if (options.output !== "json") {
1364
+ console.log(`
1365
+ \u26A0\uFE0F ${error}
1366
+ `);
1367
+ }
1368
+ return { success: false, error };
1369
+ }
1370
+ try {
1371
+ new URL(url);
1372
+ } catch {
1373
+ const error = "Invalid URL format. Please provide a valid URL.";
1374
+ if (options.output !== "json") {
1375
+ console.log(`
1376
+ \u274C ${error}
1377
+ `);
1378
+ }
1379
+ return { success: false, error };
1380
+ }
1381
+ const loggedIn = isLoggedIn();
1382
+ const userInfo = getUserInfo();
1383
+ const tier = userInfo.planName?.toLowerCase() || "free";
1384
+ const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
1385
+ if (!options.dryRun && !hasAutoFix) {
1386
+ if (options.output !== "json") {
1387
+ showUpsell("auto_fix_blocked");
1388
+ }
1389
+ return {
1390
+ success: false,
1391
+ error: "Auto-fix requires Solo tier or higher. Please upgrade at " + WEB_APP_URL + "/account"
1392
+ };
1393
+ }
1394
+ if (options.output !== "json") {
1395
+ console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
1396
+ console.log("\u2550".repeat(50));
1397
+ }
1398
+ const frameworkSpinner = options.output !== "json" ? (0, import_ora2.default)("Detecting framework...").start() : null;
1399
+ const frameworkResult = await (0, import_agent_runtime3.detectFramework)({ cwd });
1400
+ const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
1401
+ frameworkSpinner?.succeed(`Framework: ${framework.name}`);
1402
+ const analyzeSpinner = options.output !== "json" ? (0, import_ora2.default)(`Analyzing ${url}...`).start() : null;
1403
+ let issues;
1404
+ let score;
1405
+ try {
1406
+ const analysis = await (0, import_agent_runtime3.runDirectAnalysis)({ url });
1407
+ issues = analysis.issues;
1408
+ score = analysis.score;
1409
+ } catch (error) {
1410
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
1411
+ analyzeSpinner?.fail(`Analysis failed: ${errorMsg}`);
1412
+ return { success: false, error: `Analysis failed: ${errorMsg}` };
1413
+ }
1414
+ analyzeSpinner?.succeed(`Score: ${score}/100, Issues: ${issues.length}`);
1415
+ if (options.only && options.only.length > 0) {
1416
+ const onlyCodes = new Set(options.only.map((c) => c.toUpperCase()));
1417
+ issues = issues.filter((i) => onlyCodes.has(i.code.toUpperCase()));
1418
+ if (options.output !== "json") {
1419
+ console.log(import_chalk3.default.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
1420
+ }
1421
+ }
1422
+ if (issues.length === 0) {
1423
+ if (options.output !== "json") {
1424
+ console.log("\n\u2705 No issues found! Your site looks great.\n");
1425
+ }
1426
+ return {
1427
+ success: true,
1428
+ url,
1429
+ dryRun: options.dryRun,
1430
+ fixes: [],
1431
+ appliedCount: 0,
1432
+ skippedCount: 0,
1433
+ durationMs: Date.now() - startTime
1434
+ };
1435
+ }
1436
+ const fixSpinner = options.output !== "json" ? (0, import_ora2.default)("Generating fixes...").start() : null;
1437
+ const fixes = await (0, import_agent_runtime3.generateFixes)(issues, {
1438
+ cwd,
1439
+ url,
1440
+ framework
1441
+ });
1442
+ const actionableFixes = fixes.filter((f) => !f.skipped);
1443
+ const skippedFixes = fixes.filter((f) => f.skipped);
1444
+ fixSpinner?.succeed(`Generated ${actionableFixes.length} fix(es), ${skippedFixes.length} skipped`);
1445
+ if (options.dryRun) {
1446
+ if (options.output !== "json") {
1447
+ console.log("\n" + "\u2500".repeat(50));
1448
+ console.log(import_chalk3.default.yellow("DRY RUN - No changes will be made"));
1449
+ console.log("\u2500".repeat(50));
1450
+ for (let i = 0; i < fixes.length; i++) {
1451
+ printFix2(fixes[i], i);
1452
+ }
1453
+ console.log("\n" + "\u2500".repeat(50));
1454
+ console.log(`\u{1F4CB} ${import_chalk3.default.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
1455
+ console.log(` Remove --dry-run to apply these fixes.
1456
+ `);
1457
+ }
1458
+ const result2 = {
1459
+ success: true,
1460
+ url,
1461
+ dryRun: true,
1462
+ fixes,
1463
+ appliedCount: 0,
1464
+ skippedCount: skippedFixes.length,
1465
+ durationMs: Date.now() - startTime
1466
+ };
1467
+ if (options.output === "json") {
1468
+ result2.json = JSON.stringify({
1469
+ url,
1470
+ dryRun: true,
1471
+ fixes: fixes.map((f) => ({
1472
+ code: f.issue.code,
1473
+ message: f.issue.message,
1474
+ file: f.file,
1475
+ skipped: f.skipped,
1476
+ skipReason: f.skipReason
1477
+ })),
1478
+ appliedCount: 0,
1479
+ skippedCount: skippedFixes.length,
1480
+ durationMs: result2.durationMs
1481
+ }, null, 2);
1482
+ }
1483
+ return result2;
1484
+ }
1485
+ if (actionableFixes.length === 0) {
1486
+ if (options.output !== "json") {
1487
+ console.log("\n\u26A0\uFE0F No automatic fixes available. Please address issues manually.\n");
1488
+ }
1489
+ return {
1490
+ success: true,
1491
+ url,
1492
+ fixes,
1493
+ appliedCount: 0,
1494
+ skippedCount: skippedFixes.length,
1495
+ durationMs: Date.now() - startTime
1496
+ };
1497
+ }
1498
+ if (!options.auto) {
1499
+ if (options.output !== "json") {
1500
+ console.log("\n" + "\u2500".repeat(50));
1501
+ console.log("PROPOSED FIXES:");
1502
+ console.log("\u2500".repeat(50));
1503
+ for (let i = 0; i < fixes.length; i++) {
1504
+ printFix2(fixes[i], i);
1505
+ }
1506
+ console.log("\n" + "\u2500".repeat(50));
1507
+ console.log(`Ready to apply ${actionableFixes.length} fix(es).`);
1508
+ console.log("Run with --auto to apply without confirmation.");
1509
+ console.log("Run with --dry-run to preview changes.\n");
1510
+ }
1511
+ return {
1512
+ success: true,
1513
+ url,
1514
+ fixes,
1515
+ appliedCount: 0,
1516
+ skippedCount: skippedFixes.length,
1517
+ durationMs: Date.now() - startTime
1518
+ };
1519
+ }
1520
+ const applySpinner = options.output !== "json" ? (0, import_ora2.default)("Applying fixes...").start() : null;
1521
+ const { applied, skipped } = await (0, import_agent_runtime3.applyFixes)(fixes, { cwd, dryRun: false });
1522
+ for (const fix2 of applied) {
1523
+ fix2.applied = true;
1524
+ }
1525
+ applySpinner?.succeed(`Applied ${applied.length} fix(es)`);
1526
+ const commitMessage = options.commit ? generateCommitMessage(fixes, url) : void 0;
1527
+ let committed = false;
1528
+ let pushed = false;
1529
+ let commitError;
1530
+ let pushError;
1531
+ if (options.commit && applied.length > 0 && commitMessage) {
1532
+ const gitSpinner = options.output !== "json" ? (0, import_ora2.default)("Committing changes...").start() : null;
1533
+ const gitResult = await executeGitCommit(cwd, commitMessage, options.push || false);
1534
+ committed = gitResult.committed;
1535
+ pushed = gitResult.pushed;
1536
+ if (gitResult.error) {
1537
+ if (gitResult.error.includes("Push")) {
1538
+ pushError = gitResult.error;
1539
+ gitSpinner?.warn("Committed but push failed");
1540
+ } else {
1541
+ commitError = gitResult.error;
1542
+ gitSpinner?.warn(`Commit failed: ${gitResult.error}`);
1543
+ }
1544
+ } else {
1545
+ if (pushed) {
1546
+ gitSpinner?.succeed("Committed and pushed");
1547
+ } else {
1548
+ gitSpinner?.succeed("Committed");
1549
+ }
1550
+ }
1551
+ }
1552
+ if (options.output !== "json") {
1553
+ console.log("\n" + "\u2550".repeat(50));
1554
+ console.log("RESULTS");
1555
+ console.log("\u2550".repeat(50));
1556
+ if (applied.length > 0) {
1557
+ console.log(`
1558
+ ${import_chalk3.default.green("\u2705")} Applied ${applied.length} fix(es):`);
1559
+ for (const fix2 of applied) {
1560
+ console.log(` - ${fix2.file}: ${fix2.issue.message}`);
1561
+ }
1562
+ }
1563
+ if (skipped.length > 0) {
1564
+ console.log(`
1565
+ ${import_chalk3.default.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
1566
+ for (const fix2 of skipped) {
1567
+ console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
1568
+ }
1569
+ }
1570
+ if (committed) {
1571
+ console.log(`
1572
+ ${import_chalk3.default.green("\u2705")} Changes committed`);
1573
+ if (pushed) {
1574
+ console.log(`${import_chalk3.default.green("\u2705")} Changes pushed`);
1575
+ }
1576
+ }
1577
+ if (!committed && applied.length > 0) {
1578
+ console.log("\n" + "\u2500".repeat(50));
1579
+ console.log("NEXT STEPS:");
1580
+ console.log("\u2500".repeat(50));
1581
+ console.log("1. Review the changes:");
1582
+ console.log(" git diff");
1583
+ console.log("\n2. Commit the SEO improvements:");
1584
+ console.log(` git add -A && git commit -m "feat(seo): fix ${applied.length} SEO issues"`);
1585
+ console.log("\n3. Re-run the audit to verify:");
1586
+ console.log(` rankcli audit --url ${url}`);
1587
+ }
1588
+ console.log("");
1589
+ }
1590
+ const result = {
1591
+ success: true,
1592
+ url,
1593
+ dryRun: false,
1594
+ fixes,
1595
+ appliedCount: applied.length,
1596
+ skippedCount: skipped.length,
1597
+ durationMs: Date.now() - startTime,
1598
+ commitMessage,
1599
+ committed,
1600
+ pushed,
1601
+ commitError,
1602
+ pushError
1603
+ };
1604
+ if (options.output === "json") {
1605
+ result.json = JSON.stringify({
1606
+ url,
1607
+ fixes: fixes.map((f) => ({
1608
+ code: f.issue.code,
1609
+ message: f.issue.message,
1610
+ file: f.file,
1611
+ applied: f.applied,
1612
+ skipped: f.skipped,
1613
+ skipReason: f.skipReason
1614
+ })),
1615
+ appliedCount: applied.length,
1616
+ skippedCount: skipped.length,
1617
+ durationMs: result.durationMs,
1618
+ committed,
1619
+ pushed,
1620
+ commitMessage
1621
+ }, null, 2);
1622
+ }
1623
+ return result;
1624
+ }
1625
+
1626
+ // src/commands/track.ts
1627
+ var import_ora3 = __toESM(require("ora"));
1628
+ var import_chalk4 = __toESM(require("chalk"));
1629
+ var import_table = require("table");
1630
+ var import_agent_runtime4 = require("@rankcli/agent-runtime");
1631
+ async function getProjectForUrl(url) {
1632
+ const supabase = await getAuthenticatedClient();
1633
+ if (!supabase) return null;
1634
+ const { userId } = getUserInfo();
1635
+ if (!userId) return null;
1636
+ const parsedUrl = new URL(url);
1637
+ const domain = parsedUrl.hostname.replace(/^www\./, "");
1638
+ const { data, error } = await supabase.from("projects").select("id, domain, last_rank_check_at").eq("user_id", userId).eq("domain", domain).single();
1639
+ if (error || !data) {
1640
+ const { data: newProject, error: createError } = await supabase.from("projects").insert({
1641
+ user_id: userId,
1642
+ name: domain,
1643
+ url,
1644
+ domain
1645
+ }).select("id, domain, last_rank_check_at").single();
1646
+ if (createError || !newProject) return null;
1647
+ return {
1648
+ id: newProject.id,
1649
+ domain: newProject.domain,
1650
+ lastRankCheckAt: newProject.last_rank_check_at ? new Date(newProject.last_rank_check_at) : null
1651
+ };
1652
+ }
1653
+ return {
1654
+ id: data.id,
1655
+ domain: data.domain,
1656
+ lastRankCheckAt: data.last_rank_check_at ? new Date(data.last_rank_check_at) : null
1657
+ };
1658
+ }
1659
+ function getTierLimits(tier) {
1660
+ const limits = import_agent_runtime4.TIER_LIMITS[tier] || import_agent_runtime4.TIER_LIMITS.free;
1661
+ return {
1662
+ maxKeywords: limits.maxKeywords,
1663
+ checksPerDay: limits.checksPerDay
1664
+ };
1665
+ }
1666
+ function formatPosition(pos) {
1667
+ if (pos === null) return import_chalk4.default.dim("N/A");
1668
+ if (pos <= 3) return import_chalk4.default.green.bold(`#${pos}`);
1669
+ if (pos <= 10) return import_chalk4.default.green(`#${pos}`);
1670
+ if (pos <= 20) return import_chalk4.default.yellow(`#${pos}`);
1671
+ if (pos <= 50) return import_chalk4.default.red(`#${pos}`);
1672
+ return import_chalk4.default.red.dim(`#${pos}`);
1673
+ }
1674
+ function formatChange(change) {
1675
+ if (change === 0) return import_chalk4.default.dim("\u2192");
1676
+ if (change > 0) return import_chalk4.default.green(`\u2191${change}`);
1677
+ return import_chalk4.default.red(`\u2193${Math.abs(change)}`);
1678
+ }
1679
+ async function trackAdd(options) {
1680
+ if (!options.keywords || options.keywords.length === 0) {
1681
+ console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
1682
+ console.log(' rankcli track add "seo tool" "cli seo" --url https://your-site.com\n');
1683
+ return { success: false, error: "No keywords provided" };
1684
+ }
1685
+ if (!options.url) {
1686
+ console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
1687
+ console.log(' rankcli track add "keyword" --url https://your-site.com\n');
1688
+ return { success: false, error: "No URL provided" };
1689
+ }
1690
+ if (!isLoggedIn()) {
1691
+ console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
1692
+ console.log(" rankcli login\n");
1693
+ return { success: false, error: "Not logged in" };
1694
+ }
1695
+ const spinner = (0, import_ora3.default)("Adding keywords to track...").start();
1696
+ try {
1697
+ const project = await getProjectForUrl(options.url);
1698
+ if (!project) {
1699
+ spinner.fail("Could not find or create project");
1700
+ return { success: false, error: "Project not found" };
1701
+ }
1702
+ const { planName } = getUserInfo();
1703
+ const tier = planName?.toLowerCase() || "free";
1704
+ const { maxKeywords } = getTierLimits(tier);
1705
+ const supabase = await getAuthenticatedClient();
1706
+ if (!supabase) {
1707
+ spinner.fail("Could not connect to database");
1708
+ return { success: false, error: "Database connection failed" };
1709
+ }
1710
+ const { count } = await supabase.from("keywords").select("id", { count: "exact", head: true }).eq("project_id", project.id).eq("is_active", true);
1711
+ const existingCount = count || 0;
1712
+ const newCount = options.keywords.length;
1713
+ if (existingCount + newCount > maxKeywords) {
1714
+ spinner.fail(`Keyword limit reached (${existingCount}/${maxKeywords})`);
1715
+ showUpsell("keyword_limit", { tier, current: existingCount, limit: maxKeywords });
1716
+ return { success: false, error: `Keyword limit reached. Upgrade to track more keywords.` };
1717
+ }
1718
+ const keywordRecords = options.keywords.map((keyword) => ({
1719
+ project_id: project.id,
1720
+ keyword: keyword.toLowerCase().trim(),
1721
+ search_engine: options.searchEngine || "google",
1722
+ country: options.country || "US",
1723
+ language: options.language || "en",
1724
+ is_active: true
1725
+ }));
1726
+ const { data, error } = await supabase.from("keywords").upsert(keywordRecords, {
1727
+ onConflict: "project_id,keyword",
1728
+ ignoreDuplicates: false
1729
+ }).select();
1730
+ if (error) {
1731
+ spinner.fail(`Failed to add keywords: ${error.message}`);
1732
+ return { success: false, error: error.message };
1733
+ }
1734
+ const addedCount = data?.length || 0;
1735
+ spinner.succeed(`Added ${addedCount} keyword(s) to track`);
1736
+ console.log(import_chalk4.default.dim(`
1737
+ Tracking: ${options.keywords.join(", ")}`));
1738
+ console.log(import_chalk4.default.dim(`For: ${project.domain}`));
1739
+ console.log(import_chalk4.default.cyan(`
1740
+ Run 'rankcli track check --url ${options.url}' to check current positions.
1741
+ `));
1742
+ return { success: true, addedCount };
1743
+ } catch (error) {
1744
+ spinner.fail("Failed to add keywords");
1745
+ return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
1746
+ }
1747
+ }
1748
+ async function trackStatus(options) {
1749
+ if (!options.url) {
1750
+ console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
1751
+ console.log(" rankcli track status --url https://your-site.com\n");
1752
+ return { success: false, error: "No URL provided" };
1753
+ }
1754
+ if (!isLoggedIn()) {
1755
+ console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
1756
+ console.log(" rankcli login\n");
1757
+ return { success: false, error: "Not logged in" };
1758
+ }
1759
+ const spinner = (0, import_ora3.default)("Loading keyword rankings...").start();
1760
+ try {
1761
+ const project = await getProjectForUrl(options.url);
1762
+ if (!project) {
1763
+ spinner.fail("Project not found");
1764
+ return { success: false, error: "Project not found" };
1765
+ }
1766
+ const supabase = await getAuthenticatedClient();
1767
+ if (!supabase) {
1768
+ spinner.fail("Could not connect to database");
1769
+ return { success: false, error: "Database connection failed" };
1770
+ }
1771
+ const { data, error } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true).order("current_position", { ascending: true, nullsFirst: false });
1772
+ if (error) {
1773
+ spinner.fail(`Failed to load keywords: ${error.message}`);
1774
+ return { success: false, error: error.message };
1775
+ }
1776
+ spinner.succeed(`Loaded ${data?.length || 0} tracked keywords`);
1777
+ if (!data || data.length === 0) {
1778
+ console.log(import_chalk4.default.dim("\nNo keywords tracked yet."));
1779
+ console.log(import_chalk4.default.cyan(`Run 'rankcli track add "keyword" --url ${options.url}' to start tracking.
1780
+ `));
1781
+ return { success: true, keywords: [] };
1782
+ }
1783
+ const keywords2 = data.map((row) => ({
1784
+ keyword: row.keyword,
1785
+ currentPosition: row.current_position,
1786
+ previousPosition: row.previous_position,
1787
+ bestPosition: row.best_position,
1788
+ change: (row.previous_position || 0) - (row.current_position || 0),
1789
+ lastChecked: row.last_checked ? new Date(row.last_checked) : null
1790
+ }));
1791
+ if (options.output === "json") {
1792
+ console.log(JSON.stringify({ keywords: keywords2 }, null, 2));
1793
+ } else {
1794
+ console.log("\n" + import_chalk4.default.bold("\u{1F4CA} Keyword Rankings"));
1795
+ console.log("\u2550".repeat(60));
1796
+ const tableData = [
1797
+ ["Keyword", "Position", "Best", "Change", "Last Checked"],
1798
+ ...keywords2.map((kw) => [
1799
+ kw.keyword,
1800
+ formatPosition(kw.currentPosition),
1801
+ formatPosition(kw.bestPosition),
1802
+ formatChange(kw.change),
1803
+ kw.lastChecked ? kw.lastChecked.toLocaleDateString() : "Never"
1804
+ ])
1805
+ ];
1806
+ console.log((0, import_table.table)(tableData, {
1807
+ border: {
1808
+ topBody: "\u2500",
1809
+ topJoin: "\u252C",
1810
+ topLeft: "\u250C",
1811
+ topRight: "\u2510",
1812
+ bottomBody: "\u2500",
1813
+ bottomJoin: "\u2534",
1814
+ bottomLeft: "\u2514",
1815
+ bottomRight: "\u2518",
1816
+ bodyLeft: "\u2502",
1817
+ bodyRight: "\u2502",
1818
+ bodyJoin: "\u2502",
1819
+ joinBody: "\u2500",
1820
+ joinLeft: "\u251C",
1821
+ joinRight: "\u2524",
1822
+ joinJoin: "\u253C"
1823
+ }
1824
+ }));
1825
+ }
1826
+ return { success: true, keywords: keywords2 };
1827
+ } catch (error) {
1828
+ spinner.fail("Failed to load rankings");
1829
+ return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
1830
+ }
1831
+ }
1832
+ async function trackHistory(options) {
1833
+ if (!options.url) {
1834
+ return { success: false, error: "No URL provided" };
1835
+ }
1836
+ if (!options.keyword) {
1837
+ console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
1838
+ console.log(' rankcli track history --keyword "seo tool" --url https://your-site.com\n');
1839
+ return { success: false, error: "No keyword provided" };
1840
+ }
1841
+ if (!isLoggedIn()) {
1842
+ return { success: false, error: "Not logged in" };
1843
+ }
1844
+ const spinner = (0, import_ora3.default)(`Loading history for "${options.keyword}"...`).start();
1845
+ try {
1846
+ const project = await getProjectForUrl(options.url);
1847
+ if (!project) {
1848
+ spinner.fail("Project not found");
1849
+ return { success: false, error: "Project not found" };
1850
+ }
1851
+ const supabase = await getAuthenticatedClient();
1852
+ if (!supabase) {
1853
+ spinner.fail("Could not connect to database");
1854
+ return { success: false, error: "Database connection failed" };
1855
+ }
1856
+ const { data: keywordData } = await supabase.from("keywords").select("id, keyword").eq("project_id", project.id).eq("keyword", options.keyword.toLowerCase().trim()).single();
1857
+ if (!keywordData) {
1858
+ spinner.fail("Keyword not found");
1859
+ return { success: false, error: "Keyword not found" };
1860
+ }
1861
+ const days = options.days || 30;
1862
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
1863
+ const { data: historyData, error } = await supabase.from("rankings").select("position, url, recorded_at").eq("keyword_id", keywordData.id).gte("recorded_at", cutoff.toISOString()).order("recorded_at", { ascending: false });
1864
+ if (error) {
1865
+ spinner.fail(`Failed to load history: ${error.message}`);
1866
+ return { success: false, error: error.message };
1867
+ }
1868
+ spinner.succeed(`Loaded ${historyData?.length || 0} data points`);
1869
+ const history = (historyData || []).map((row) => ({
1870
+ position: row.position,
1871
+ url: row.url,
1872
+ recordedAt: new Date(row.recorded_at)
1873
+ }));
1874
+ if (options.output === "json") {
1875
+ console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
1876
+ } else {
1877
+ console.log("\n" + import_chalk4.default.bold(`\u{1F4C8} History: "${options.keyword}"`));
1878
+ console.log("\u2550".repeat(50));
1879
+ if (history.length === 0) {
1880
+ console.log(import_chalk4.default.dim("\nNo history available yet.\n"));
1881
+ } else {
1882
+ const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
1883
+ const sparkline = positions.map((p) => {
1884
+ if (p <= 10) return import_chalk4.default.green("\u2588");
1885
+ if (p <= 20) return import_chalk4.default.yellow("\u2593");
1886
+ if (p <= 50) return import_chalk4.default.red("\u2592");
1887
+ return import_chalk4.default.dim("\u2591");
1888
+ }).join("");
1889
+ console.log(`
1890
+ ${sparkline} (last ${positions.length} checks)`);
1891
+ console.log(import_chalk4.default.dim(" \u2190 newer older \u2192\n"));
1892
+ const recent = history.slice(0, 10);
1893
+ for (const entry of recent) {
1894
+ console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
1895
+ }
1896
+ }
1897
+ }
1898
+ return { success: true, history };
1899
+ } catch (error) {
1900
+ spinner.fail("Failed to load history");
1901
+ return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
1902
+ }
1903
+ }
1904
+ async function trackExport(options) {
1905
+ if (!options.url) {
1906
+ return { success: false, error: "No URL provided" };
1907
+ }
1908
+ if (!isLoggedIn()) {
1909
+ return { success: false, error: "Not logged in" };
1910
+ }
1911
+ const spinner = (0, import_ora3.default)("Exporting data...").start();
1912
+ try {
1913
+ const project = await getProjectForUrl(options.url);
1914
+ if (!project) {
1915
+ spinner.fail("Project not found");
1916
+ return { success: false, error: "Project not found" };
1917
+ }
1918
+ const supabase = await getAuthenticatedClient();
1919
+ if (!supabase) {
1920
+ spinner.fail("Could not connect to database");
1921
+ return { success: false, error: "Database connection failed" };
1922
+ }
1923
+ const { data: keywords2, error } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true);
1924
+ if (error) {
1925
+ spinner.fail(`Failed to export: ${error.message}`);
1926
+ return { success: false, error: error.message };
1927
+ }
1928
+ const format = options.format || "csv";
1929
+ if (format === "json") {
1930
+ const data = JSON.stringify({
1931
+ domain: project.domain,
1932
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1933
+ keywords: keywords2?.map((kw) => ({
1934
+ keyword: kw.keyword,
1935
+ currentPosition: kw.current_position,
1936
+ bestPosition: kw.best_position,
1937
+ lastChecked: kw.last_checked
1938
+ }))
1939
+ }, null, 2);
1940
+ spinner.succeed("Exported as JSON");
1941
+ console.log(data);
1942
+ return { success: true, data };
1943
+ } else {
1944
+ const rows = ["Keyword,Current Position,Best Position,Last Checked,Trend"];
1945
+ for (const kw of keywords2 || []) {
1946
+ const trend = (kw.previous_position || 0) - (kw.current_position || 0);
1947
+ const trendStr = trend > 0 ? "\u2191" : trend < 0 ? "\u2193" : "\u2192";
1948
+ rows.push([
1949
+ `"${kw.keyword}"`,
1950
+ kw.current_position?.toString() || "N/A",
1951
+ kw.best_position?.toString() || "N/A",
1952
+ kw.last_checked || "Never",
1953
+ trendStr
1954
+ ].join(","));
1955
+ }
1956
+ const data = rows.join("\n");
1957
+ spinner.succeed("Exported as CSV");
1958
+ console.log(data);
1959
+ return { success: true, data };
1960
+ }
1961
+ } catch (error) {
1962
+ spinner.fail("Failed to export");
1963
+ return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
1964
+ }
1965
+ }
1966
+ async function trackCheck(options) {
1967
+ if (!options.url) {
1968
+ console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
1969
+ console.log(" rankcli track check --url https://your-site.com\n");
1970
+ return { success: false, error: "No URL provided" };
1971
+ }
1972
+ if (!isLoggedIn()) {
1973
+ console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
1974
+ console.log(" rankcli login\n");
1975
+ return { success: false, error: "Not logged in" };
1976
+ }
1977
+ try {
1978
+ const project = await getProjectForUrl(options.url);
1979
+ if (!project) {
1980
+ return { success: false, error: "Project not found" };
1981
+ }
1982
+ const { planName } = getUserInfo();
1983
+ const tier = planName?.toLowerCase() || "free";
1984
+ const { checksPerDay } = getTierLimits(tier);
1985
+ if (!options.force && project.lastRankCheckAt) {
1986
+ const hoursSinceCheck = (Date.now() - project.lastRankCheckAt.getTime()) / (1e3 * 60 * 60);
1987
+ const minHoursBetweenChecks = 24 / checksPerDay;
1988
+ if (hoursSinceCheck < minHoursBetweenChecks) {
1989
+ const nextCheck = new Date(project.lastRankCheckAt.getTime() + minHoursBetweenChecks * 60 * 60 * 1e3);
1990
+ console.log(import_chalk4.default.yellow(`
1991
+ \u26A0\uFE0F Rate limited. Next check available: ${nextCheck.toLocaleString()}`));
1992
+ console.log(import_chalk4.default.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
1993
+ `));
1994
+ return { success: true, rateLimited: true };
1995
+ }
1996
+ }
1997
+ const spinner = (0, import_ora3.default)("Checking current rankings...").start();
1998
+ const supabase = await getAuthenticatedClient();
1999
+ if (!supabase) {
2000
+ spinner.fail("Could not connect to database");
2001
+ return { success: false, error: "Database connection failed" };
2002
+ }
2003
+ const { data: keywords2 } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true);
2004
+ if (!keywords2 || keywords2.length === 0) {
2005
+ spinner.fail("No keywords to check");
2006
+ return { success: false, error: "No keywords tracked" };
2007
+ }
2008
+ const serpApiKey = process.env.VALUESERP_API_KEY || process.env.SERPAPI_KEY;
2009
+ if (!serpApiKey) {
2010
+ spinner.warn("No SERP API key configured");
2011
+ console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Set VALUESERP_API_KEY or SERPAPI_KEY to check live rankings."));
2012
+ console.log(import_chalk4.default.dim(" Get a key at https://www.valueserp.com or https://serpapi.com\n"));
2013
+ return { success: false, error: "No SERP API key" };
2014
+ }
2015
+ const tracker = new import_agent_runtime4.RankTracker({
2016
+ supabase,
2017
+ serpConfig: {
2018
+ provider: process.env.VALUESERP_API_KEY ? "valueserp" : "serpapi",
2019
+ apiKey: serpApiKey
2020
+ }
2021
+ });
2022
+ const results = await tracker.checkRankings(project.id, project.domain);
2023
+ spinner.succeed(`Checked ${results.length} keyword(s)`);
2024
+ console.log("\n" + import_chalk4.default.bold("\u{1F50D} Rank Check Results"));
2025
+ console.log("\u2550".repeat(50));
2026
+ for (const result of results) {
2027
+ const posStr = formatPosition(result.position);
2028
+ console.log(` ${result.keyword}: ${posStr}`);
2029
+ if (result.serpFeatures.length > 0) {
2030
+ console.log(import_chalk4.default.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
2031
+ }
2032
+ }
2033
+ console.log("");
2034
+ return {
2035
+ success: true,
2036
+ checkResults: results.map((r) => ({
2037
+ keyword: r.keyword,
2038
+ position: r.position,
2039
+ url: r.url
2040
+ }))
2041
+ };
2042
+ } catch (error) {
2043
+ return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
2044
+ }
2045
+ }
2046
+
2047
+ // src/commands/geo.ts
2048
+ var import_agent_runtime5 = require("@rankcli/agent-runtime");
2049
+ var import_ora4 = __toESM(require("ora"));
2050
+ var import_chalk5 = __toESM(require("chalk"));
1058
2051
  async function fetchResources(url) {
1059
2052
  const headers = {};
1060
2053
  let html = null;
@@ -1100,7 +2093,7 @@ async function geo(options) {
1100
2093
  console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
1101
2094
  return;
1102
2095
  }
1103
- const spinner = (0, import_ora2.default)("Fetching page and robots.txt...").start();
2096
+ const spinner = (0, import_ora4.default)("Fetching page and robots.txt...").start();
1104
2097
  const { html, robotsTxt, headers } = await fetchResources(url);
1105
2098
  if (!html) {
1106
2099
  spinner.fail("Could not fetch page content");
@@ -1108,11 +2101,11 @@ async function geo(options) {
1108
2101
  }
1109
2102
  spinner.text = "Running GEO analysis...";
1110
2103
  try {
1111
- const geoResult = await import_agent_runtime3.analyzers.analyzeGEO(html, url, robotsTxt || void 0);
1112
- const cwvResult = import_agent_runtime3.analyzers.analyzeCoreWebVitals(html, url, headers);
1113
- const securityResult = import_agent_runtime3.analyzers.analyzeSecurityHeaders(headers, url);
1114
- const schemaResult = import_agent_runtime3.analyzers.analyzeStructuredData(html, url);
1115
- const mobileResult = import_agent_runtime3.analyzers.analyzeMobileSEO(html, url);
2104
+ const geoResult = await import_agent_runtime5.analyzers.analyzeGEO(html, url, robotsTxt || void 0);
2105
+ const cwvResult = import_agent_runtime5.analyzers.analyzeCoreWebVitals(html, url, headers);
2106
+ const securityResult = import_agent_runtime5.analyzers.analyzeSecurityHeaders(headers, url);
2107
+ const schemaResult = import_agent_runtime5.analyzers.analyzeStructuredData(html, url);
2108
+ const mobileResult = import_agent_runtime5.analyzers.analyzeMobileSEO(html, url);
1116
2109
  spinner.succeed("GEO analysis complete");
1117
2110
  if (options.output === "json") {
1118
2111
  const jsonOutput = {
@@ -1126,55 +2119,55 @@ async function geo(options) {
1126
2119
  console.log(JSON.stringify(jsonOutput, null, 2));
1127
2120
  return;
1128
2121
  }
1129
- console.log("\n" + import_chalk2.default.bold("\u2550".repeat(60)));
1130
- console.log(import_chalk2.default.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
1131
- console.log(import_chalk2.default.bold("\u2550".repeat(60)));
2122
+ console.log("\n" + import_chalk5.default.bold("\u2550".repeat(60)));
2123
+ console.log(import_chalk5.default.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
2124
+ console.log(import_chalk5.default.bold("\u2550".repeat(60)));
1132
2125
  const geoScore = geoResult.score;
1133
- const scoreColor = geoScore >= 80 ? import_chalk2.default.green : geoScore >= 60 ? import_chalk2.default.yellow : import_chalk2.default.red;
2126
+ const scoreColor = geoScore >= 80 ? import_chalk5.default.green : geoScore >= 60 ? import_chalk5.default.yellow : import_chalk5.default.red;
1134
2127
  console.log(`
1135
- ${import_chalk2.default.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
1136
- console.log("\n" + import_chalk2.default.bold(" AI Crawler Access:"));
2128
+ ${import_chalk5.default.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
2129
+ console.log("\n" + import_chalk5.default.bold(" AI Crawler Access:"));
1137
2130
  const { aiCrawlerAccess } = geoResult;
1138
2131
  if (aiCrawlerAccess.blockedCrawlers.length > 0) {
1139
- console.log(import_chalk2.default.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
2132
+ console.log(import_chalk5.default.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
1140
2133
  }
1141
2134
  if (aiCrawlerAccess.allowedCrawlers.length > 0) {
1142
2135
  const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
1143
- console.log(import_chalk2.default.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
2136
+ console.log(import_chalk5.default.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
1144
2137
  }
1145
- console.log("\n" + import_chalk2.default.bold(" Rendering:"));
2138
+ console.log("\n" + import_chalk5.default.bold(" Rendering:"));
1146
2139
  if (aiCrawlerAccess.serverSideRendered) {
1147
- console.log(import_chalk2.default.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
2140
+ console.log(import_chalk5.default.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
1148
2141
  } else if (aiCrawlerAccess.jsRenderingRequired) {
1149
- console.log(import_chalk2.default.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
2142
+ console.log(import_chalk5.default.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
1150
2143
  }
1151
- console.log("\n" + import_chalk2.default.bold(" LLM Friendliness:"));
2144
+ console.log("\n" + import_chalk5.default.bold(" LLM Friendliness:"));
1152
2145
  const { llmSignals } = geoResult;
1153
2146
  const bar = (score) => {
1154
2147
  const filled = Math.round(score / 5);
1155
2148
  const empty = 20 - filled;
1156
- const color = score >= 70 ? import_chalk2.default.green : score >= 40 ? import_chalk2.default.yellow : import_chalk2.default.red;
1157
- return color("\u2588".repeat(filled)) + import_chalk2.default.gray("\u2591".repeat(empty));
2149
+ const color = score >= 70 ? import_chalk5.default.green : score >= 40 ? import_chalk5.default.yellow : import_chalk5.default.red;
2150
+ return color("\u2588".repeat(filled)) + import_chalk5.default.gray("\u2591".repeat(empty));
1158
2151
  };
1159
2152
  console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
1160
2153
  console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
1161
2154
  console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
1162
2155
  console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
1163
- console.log("\n" + import_chalk2.default.bold(" Content Structure:"));
2156
+ console.log("\n" + import_chalk5.default.bold(" Content Structure:"));
1164
2157
  const { contentStructure } = geoResult;
1165
- console.log(` ${contentStructure.hasStructuredData ? import_chalk2.default.green("\u2705") : import_chalk2.default.red("\u274C")} JSON-LD Structured Data`);
1166
- console.log(` ${contentStructure.hasFAQSchema ? import_chalk2.default.green("\u2705") : import_chalk2.default.yellow("\u25CB")} FAQ Schema`);
1167
- console.log(` ${contentStructure.hasArticleSchema ? import_chalk2.default.green("\u2705") : import_chalk2.default.yellow("\u25CB")} Article Schema`);
1168
- console.log(` ${contentStructure.hasBreadcrumbs ? import_chalk2.default.green("\u2705") : import_chalk2.default.yellow("\u25CB")} Breadcrumbs`);
1169
- console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? import_chalk2.default.green("Good") : contentStructure.headingHierarchy === "needs-work" ? import_chalk2.default.yellow("Needs Work") : import_chalk2.default.red("Poor")}`);
1170
- console.log("\n" + import_chalk2.default.bold(" Citation Readiness:"));
2158
+ console.log(` ${contentStructure.hasStructuredData ? import_chalk5.default.green("\u2705") : import_chalk5.default.red("\u274C")} JSON-LD Structured Data`);
2159
+ console.log(` ${contentStructure.hasFAQSchema ? import_chalk5.default.green("\u2705") : import_chalk5.default.yellow("\u25CB")} FAQ Schema`);
2160
+ console.log(` ${contentStructure.hasArticleSchema ? import_chalk5.default.green("\u2705") : import_chalk5.default.yellow("\u25CB")} Article Schema`);
2161
+ console.log(` ${contentStructure.hasBreadcrumbs ? import_chalk5.default.green("\u2705") : import_chalk5.default.yellow("\u25CB")} Breadcrumbs`);
2162
+ console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? import_chalk5.default.green("Good") : contentStructure.headingHierarchy === "needs-work" ? import_chalk5.default.yellow("Needs Work") : import_chalk5.default.red("Poor")}`);
2163
+ console.log("\n" + import_chalk5.default.bold(" Citation Readiness:"));
1171
2164
  const { citationReadiness } = geoResult;
1172
2165
  if (citationReadiness.trustSignals.length > 0) {
1173
- console.log(import_chalk2.default.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
2166
+ console.log(import_chalk5.default.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
1174
2167
  } else {
1175
- console.log(import_chalk2.default.yellow(" \u25CB No trust signals detected"));
2168
+ console.log(import_chalk5.default.yellow(" \u25CB No trust signals detected"));
1176
2169
  }
1177
- console.log("\n" + import_chalk2.default.bold(" Other Scores:"));
2170
+ console.log("\n" + import_chalk5.default.bold(" Other Scores:"));
1178
2171
  console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
1179
2172
  console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
1180
2173
  console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
@@ -1188,18 +2181,18 @@ async function geo(options) {
1188
2181
  ];
1189
2182
  const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
1190
2183
  if (criticalIssues.length > 0) {
1191
- console.log("\n" + import_chalk2.default.bold.red(" Critical Issues:"));
2184
+ console.log("\n" + import_chalk5.default.bold.red(" Critical Issues:"));
1192
2185
  criticalIssues.slice(0, 5).forEach((issue, i) => {
1193
- console.log(import_chalk2.default.red(` ${i + 1}. ${issue.title}`));
2186
+ console.log(import_chalk5.default.red(` ${i + 1}. ${issue.title}`));
1194
2187
  });
1195
2188
  }
1196
2189
  if (geoResult.recommendations.length > 0) {
1197
- console.log("\n" + import_chalk2.default.bold(" Recommendations:"));
2190
+ console.log("\n" + import_chalk5.default.bold(" Recommendations:"));
1198
2191
  geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
1199
2192
  console.log(` ${i + 1}. ${rec}`);
1200
2193
  });
1201
2194
  }
1202
- console.log("\n" + import_chalk2.default.bold("\u2550".repeat(60)));
2195
+ console.log("\n" + import_chalk5.default.bold("\u2550".repeat(60)));
1203
2196
  console.log("");
1204
2197
  } catch (error) {
1205
2198
  spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1208,7 +2201,7 @@ async function geo(options) {
1208
2201
 
1209
2202
  // src/commands/keywords.ts
1210
2203
  var readline = __toESM(require("readline"));
1211
- var import_agent_runtime4 = require("@rankcli/agent-runtime");
2204
+ var import_agent_runtime6 = require("@rankcli/agent-runtime");
1212
2205
  async function keywords(options) {
1213
2206
  console.log("\n\u{1F511} SEO Autopilot - Keyword Research\n");
1214
2207
  console.log("This tool helps you find keywords you can actually rank for,");
@@ -1217,7 +2210,7 @@ async function keywords(options) {
1217
2210
  input: process.stdin,
1218
2211
  output: process.stdout
1219
2212
  });
1220
- const ask = (question) => {
2213
+ const ask2 = (question) => {
1221
2214
  return new Promise((resolve) => {
1222
2215
  rl.question(question, (answer) => {
1223
2216
  resolve(answer.trim());
@@ -1232,7 +2225,7 @@ ${question}`);
1232
2225
  console.log(` ${choice.description}`);
1233
2226
  });
1234
2227
  while (true) {
1235
- const answer = await ask("\nEnter your choice (1-" + choices.length + "): ");
2228
+ const answer = await ask2("\nEnter your choice (1-" + choices.length + "): ");
1236
2229
  const num = parseInt(answer, 10);
1237
2230
  if (num >= 1 && num <= choices.length) {
1238
2231
  return choices[num - 1].value;
@@ -1243,7 +2236,7 @@ ${question}`);
1243
2236
  try {
1244
2237
  let url = options.url;
1245
2238
  if (!url) {
1246
- url = await ask("Enter your website URL: ");
2239
+ url = await ask2("Enter your website URL: ");
1247
2240
  }
1248
2241
  try {
1249
2242
  const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
@@ -1259,7 +2252,7 @@ ${question}`);
1259
2252
  seedKeywords = options.seed.split(",").map((s) => s.trim());
1260
2253
  } else if (options.auto) {
1261
2254
  console.log("\n\u{1F50D} Auto-extracting seed keywords from your page...");
1262
- seedKeywords = await (0, import_agent_runtime4.extractSeedKeywords)(url);
2255
+ seedKeywords = await (0, import_agent_runtime6.extractSeedKeywords)(url);
1263
2256
  if (seedKeywords.length > 0) {
1264
2257
  console.log(`Found: ${seedKeywords.join(", ")}`);
1265
2258
  }
@@ -1267,7 +2260,7 @@ ${question}`);
1267
2260
  if (seedKeywords.length === 0) {
1268
2261
  console.log("\n\u{1F4DD} What is your website/product about?");
1269
2262
  console.log(" (Enter 1-3 main topics or keywords, comma-separated)");
1270
- const seedInput = await ask(" > ");
2263
+ const seedInput = await ask2(" > ");
1271
2264
  seedKeywords = seedInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1272
2265
  if (seedKeywords.length === 0) {
1273
2266
  console.log("\n\u274C Please provide at least one seed keyword.\n");
@@ -1284,7 +2277,7 @@ ${question}`);
1284
2277
  contentCapacity: "medium",
1285
2278
  targetGeo: "us"
1286
2279
  };
1287
- for (const q of import_agent_runtime4.SITE_PROFILE_QUESTIONS) {
2280
+ for (const q of import_agent_runtime6.SITE_PROFILE_QUESTIONS) {
1288
2281
  const answer = await askChoice(q.question, q.options);
1289
2282
  siteProfile[q.id] = answer;
1290
2283
  }
@@ -1303,13 +2296,13 @@ ${question}`);
1303
2296
  url,
1304
2297
  maxKeywords: 100
1305
2298
  };
1306
- const result = await (0, import_agent_runtime4.runKeywordResearch)(researchOptions);
1307
- console.log((0, import_agent_runtime4.formatKeywordReport)(result));
2299
+ const result = await (0, import_agent_runtime6.runKeywordResearch)(researchOptions);
2300
+ console.log((0, import_agent_runtime6.formatKeywordReport)(result));
1308
2301
  const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
1309
2302
  if (allKeywords.length > 5) {
1310
2303
  console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
1311
- const topicResult = (0, import_agent_runtime4.groupKeywordsByTopic)(allKeywords);
1312
- console.log((0, import_agent_runtime4.formatTopicReport)(topicResult));
2304
+ const topicResult = (0, import_agent_runtime6.groupKeywordsByTopic)(allKeywords);
2305
+ console.log((0, import_agent_runtime6.formatTopicReport)(topicResult));
1313
2306
  }
1314
2307
  if (result.quickWins.length > 0) {
1315
2308
  console.log('\n\u{1F4A1} TIP: Run "seo apply --url ' + url + '" to apply keyword optimizations');
@@ -1334,7 +2327,7 @@ async function keywordsQuick(url, providedSeeds) {
1334
2327
  `);
1335
2328
  } else {
1336
2329
  console.log("\u{1F50D} Extracting keywords from your page...");
1337
- seedKeywords = await (0, import_agent_runtime4.extractSeedKeywords)(url);
2330
+ seedKeywords = await (0, import_agent_runtime6.extractSeedKeywords)(url);
1338
2331
  if (seedKeywords.length === 0) {
1339
2332
  console.log("\u274C Could not extract keywords. Please use interactive mode.\n");
1340
2333
  return;
@@ -1350,18 +2343,18 @@ async function keywordsQuick(url, providedSeeds) {
1350
2343
  contentCapacity: "medium",
1351
2344
  targetGeo: "us"
1352
2345
  };
1353
- const result = await (0, import_agent_runtime4.runKeywordResearch)({
2346
+ const result = await (0, import_agent_runtime6.runKeywordResearch)({
1354
2347
  seedKeywords,
1355
2348
  siteProfile,
1356
2349
  url,
1357
2350
  maxKeywords: 50
1358
2351
  });
1359
- console.log((0, import_agent_runtime4.formatKeywordReport)(result));
2352
+ console.log((0, import_agent_runtime6.formatKeywordReport)(result));
1360
2353
  const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
1361
2354
  if (allKeywords.length > 5) {
1362
2355
  console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
1363
- const topicResult = (0, import_agent_runtime4.groupKeywordsByTopic)(allKeywords);
1364
- console.log((0, import_agent_runtime4.formatTopicReport)(topicResult));
2356
+ const topicResult = (0, import_agent_runtime6.groupKeywordsByTopic)(allKeywords);
2357
+ console.log((0, import_agent_runtime6.formatTopicReport)(topicResult));
1365
2358
  }
1366
2359
  } catch (error) {
1367
2360
  console.log(`
@@ -1424,7 +2417,7 @@ ${result2.teaser.upgradeMessage}`);
1424
2417
  return;
1425
2418
  }
1426
2419
  console.log("\u{1F52C} Running local AI analysis (this may take a minute)...\n");
1427
- const result = await (0, import_agent_runtime4.runAIKeywordResearch)({
2420
+ const result = await (0, import_agent_runtime6.runAIKeywordResearch)({
1428
2421
  url,
1429
2422
  tier: "solo",
1430
2423
  openaiApiKey,
@@ -1543,12 +2536,12 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
1543
2536
  }
1544
2537
  console.log("");
1545
2538
  console.log("Analyzing competitor keywords (this may take a moment)...\n");
1546
- const result = await (0, import_agent_runtime4.discoverCompetitorKeywords)(
2539
+ const result = await (0, import_agent_runtime6.discoverCompetitorKeywords)(
1547
2540
  yourDomain,
1548
2541
  seedKeywords,
1549
2542
  competitors
1550
2543
  );
1551
- console.log((0, import_agent_runtime4.formatCompetitorReport)(result));
2544
+ console.log((0, import_agent_runtime6.formatCompetitorReport)(result));
1552
2545
  if (result.missingKeywords.length > 0) {
1553
2546
  console.log("\n\u{1F3AF} ACTION ITEMS:");
1554
2547
  console.log("\u2500".repeat(60));
@@ -1567,7 +2560,7 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
1567
2560
  }
1568
2561
 
1569
2562
  // src/commands/content.ts
1570
- var import_agent_runtime5 = require("@rankcli/agent-runtime");
2563
+ var import_agent_runtime7 = require("@rankcli/agent-runtime");
1571
2564
  async function analyzeContent(options) {
1572
2565
  console.log("\n\u{1F4DD} SEO Autopilot - Content Analysis\n");
1573
2566
  const mode = options.mode || "full";
@@ -1577,12 +2570,12 @@ async function analyzeContent(options) {
1577
2570
  console.log('\u274C Please provide a headline with --headline "Your Headline Here"');
1578
2571
  return;
1579
2572
  }
1580
- const analysis = (0, import_agent_runtime5.analyzeHeadline)(headline);
1581
- console.log((0, import_agent_runtime5.formatHeadlineReport)(analysis));
2573
+ const analysis = (0, import_agent_runtime7.analyzeHeadline)(headline);
2574
+ console.log((0, import_agent_runtime7.formatHeadlineReport)(analysis));
1582
2575
  if (options.keyword) {
1583
2576
  console.log("\n\u{1F4CB} HEADLINE VARIATIONS");
1584
2577
  console.log("\u2500".repeat(60));
1585
- const variations = (0, import_agent_runtime5.generateHeadlineVariations)(options.keyword, [options.keyword]);
2578
+ const variations = (0, import_agent_runtime7.generateHeadlineVariations)(options.keyword, [options.keyword]);
1586
2579
  for (const v of variations.slice(0, 5)) {
1587
2580
  console.log(` \u2022 ${v}`);
1588
2581
  }
@@ -1593,7 +2586,7 @@ async function analyzeContent(options) {
1593
2586
  if (options.url) {
1594
2587
  console.log(`Fetching content from ${options.url}...`);
1595
2588
  try {
1596
- const result = await (0, import_agent_runtime5.crawlUrl)(options.url);
2589
+ const result = await (0, import_agent_runtime7.crawlUrl)(options.url);
1597
2590
  if (!result.html) {
1598
2591
  console.log("\u274C Could not fetch content from URL");
1599
2592
  return;
@@ -1607,15 +2600,15 @@ async function analyzeContent(options) {
1607
2600
  Analyzing: "${title || options.url}"
1608
2601
  `);
1609
2602
  if (mode === "full" || mode === "readability") {
1610
- const readability = (0, import_agent_runtime5.analyzeReadability)(bodyText);
1611
- console.log((0, import_agent_runtime5.formatReadabilityReport)(readability));
2603
+ const readability = (0, import_agent_runtime7.analyzeReadability)(bodyText);
2604
+ console.log((0, import_agent_runtime7.formatReadabilityReport)(readability));
1612
2605
  }
1613
2606
  if (mode === "full" && title) {
1614
- const headlineAnalysis = (0, import_agent_runtime5.analyzeHeadline)(title);
1615
- console.log((0, import_agent_runtime5.formatHeadlineReport)(headlineAnalysis));
2607
+ const headlineAnalysis = (0, import_agent_runtime7.analyzeHeadline)(title);
2608
+ console.log((0, import_agent_runtime7.formatHeadlineReport)(headlineAnalysis));
1616
2609
  }
1617
2610
  if (options.keyword && (mode === "full" || mode === "density")) {
1618
- const densityAnalysis = (0, import_agent_runtime5.analyzeKeywordDensity)(options.keyword, {
2611
+ const densityAnalysis = (0, import_agent_runtime7.analyzeKeywordDensity)(options.keyword, {
1619
2612
  title,
1620
2613
  h1,
1621
2614
  headings,
@@ -1623,19 +2616,19 @@ Analyzing: "${title || options.url}"
1623
2616
  firstParagraph: paragraphs[0],
1624
2617
  lastParagraph: paragraphs[paragraphs.length - 1]
1625
2618
  });
1626
- console.log((0, import_agent_runtime5.formatKeywordDensityReport)(densityAnalysis));
2619
+ console.log((0, import_agent_runtime7.formatKeywordDensityReport)(densityAnalysis));
1627
2620
  }
1628
2621
  if (options.keyword && (mode === "full" || mode === "snippet")) {
1629
- const snippetAnalysis = (0, import_agent_runtime5.analyzeFeaturedSnippetPotential)(
2622
+ const snippetAnalysis = (0, import_agent_runtime7.analyzeFeaturedSnippetPotential)(
1630
2623
  options.keyword,
1631
2624
  bodyText,
1632
2625
  headings
1633
2626
  );
1634
- console.log((0, import_agent_runtime5.formatFeaturedSnippetReport)(snippetAnalysis));
2627
+ console.log((0, import_agent_runtime7.formatFeaturedSnippetReport)(snippetAnalysis));
1635
2628
  }
1636
2629
  if (options.keyword) {
1637
- const intentAnalysis = (0, import_agent_runtime5.classifyIntent)(options.keyword);
1638
- console.log((0, import_agent_runtime5.formatIntentReport)(options.keyword, intentAnalysis));
2630
+ const intentAnalysis = (0, import_agent_runtime7.classifyIntent)(options.keyword);
2631
+ console.log((0, import_agent_runtime7.formatIntentReport)(options.keyword, intentAnalysis));
1639
2632
  }
1640
2633
  console.log("\n\u{1F4CA} CONTENT SUMMARY");
1641
2634
  console.log("\u2500".repeat(60));
@@ -1662,8 +2655,8 @@ Analyzing: "${title || options.url}"
1662
2655
  }
1663
2656
  async function analyzeIntent(keyword) {
1664
2657
  console.log("\n\u{1F50D} Search Intent Analysis\n");
1665
- const analysis = (0, import_agent_runtime5.classifyIntent)(keyword);
1666
- console.log((0, import_agent_runtime5.formatIntentReport)(keyword, analysis));
2658
+ const analysis = (0, import_agent_runtime7.classifyIntent)(keyword);
2659
+ console.log((0, import_agent_runtime7.formatIntentReport)(keyword, analysis));
1667
2660
  }
1668
2661
  function extractBodyText(html) {
1669
2662
  let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
@@ -1682,7 +2675,7 @@ function extractBodyText(html) {
1682
2675
  var readline2 = __toESM(require("readline"));
1683
2676
  var fs = __toESM(require("fs"));
1684
2677
  var path = __toESM(require("path"));
1685
- var import_agent_runtime6 = require("@rankcli/agent-runtime");
2678
+ var import_agent_runtime8 = require("@rankcli/agent-runtime");
1686
2679
  async function setup(options) {
1687
2680
  console.log("\n\u{1F527} SEO Autopilot - Setup\n");
1688
2681
  const projectPath = process.cwd();
@@ -1708,7 +2701,7 @@ async function interactiveSetup(projectPath) {
1708
2701
  input: process.stdin,
1709
2702
  output: process.stdout
1710
2703
  });
1711
- const ask = (question) => {
2704
+ const ask2 = (question) => {
1712
2705
  return new Promise((resolve) => {
1713
2706
  rl.question(question, (answer) => {
1714
2707
  resolve(answer.trim());
@@ -1722,7 +2715,7 @@ async function interactiveSetup(projectPath) {
1722
2715
  console.log("GA4 tracks visitor behavior on your site.");
1723
2716
  console.log("Get your Measurement ID from: https://analytics.google.com");
1724
2717
  console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
1725
- const ga4Id = await ask("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
2718
+ const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
1726
2719
  if (ga4Id && ga4Id.startsWith("G-")) {
1727
2720
  await setupGA4(projectPath, ga4Id);
1728
2721
  } else if (ga4Id) {
@@ -1735,7 +2728,7 @@ async function interactiveSetup(projectPath) {
1735
2728
  console.log("GSC shows how Google sees your site and what queries you rank for.");
1736
2729
  console.log("Get your verification code from: https://search.google.com/search-console");
1737
2730
  console.log("Go to: Settings > Ownership verification > HTML tag\n");
1738
- const gscCode = await ask("Enter GSC verification code or press Enter to skip: ");
2731
+ const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
1739
2732
  if (gscCode && gscCode.length > 10) {
1740
2733
  await setupGSC(projectPath, gscCode);
1741
2734
  } else if (gscCode) {
@@ -1747,20 +2740,20 @@ async function interactiveSetup(projectPath) {
1747
2740
  console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
1748
2741
  console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
1749
2742
  console.log("It creates issues with reports and can auto-fix some problems.\n");
1750
- const setupAction = await ask("Set up GitHub Action? (y/n): ");
2743
+ const setupAction = await ask2("Set up GitHub Action? (y/n): ");
1751
2744
  if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
1752
2745
  console.log("\nHow often should the check run?");
1753
2746
  console.log(" 1. Weekly (recommended for most sites)");
1754
2747
  console.log(" 2. Daily (for high-traffic sites)");
1755
2748
  console.log(" 3. Monthly (for low-traffic sites)\n");
1756
- const scheduleChoice = await ask("Enter choice (1-3): ");
2749
+ const scheduleChoice = await ask2("Enter choice (1-3): ");
1757
2750
  const scheduleMap = {
1758
2751
  "1": "weekly",
1759
2752
  "2": "daily",
1760
2753
  "3": "monthly"
1761
2754
  };
1762
2755
  const schedule = scheduleMap[scheduleChoice] || "weekly";
1763
- const siteUrl = await ask("Enter your site URL (https://example.com): ");
2756
+ const siteUrl = await ask2("Enter your site URL (https://example.com): ");
1764
2757
  if (siteUrl) {
1765
2758
  await setupGitHubAction(projectPath, schedule, siteUrl);
1766
2759
  }
@@ -1784,7 +2777,7 @@ async function interactiveSetup(projectPath) {
1784
2777
  }
1785
2778
  async function setupGA4(projectPath, measurementId) {
1786
2779
  console.log(`Setting up Google Analytics 4 (${measurementId})...`);
1787
- const result = await (0, import_agent_runtime6.injectGA4)(projectPath, { measurementId });
2780
+ const result = await (0, import_agent_runtime8.injectGA4)(projectPath, { measurementId });
1788
2781
  if (result.success) {
1789
2782
  console.log(`\u2705 ${result.message}`);
1790
2783
  if (result.file) {
@@ -1798,7 +2791,7 @@ async function setupGA4(projectPath, measurementId) {
1798
2791
  }
1799
2792
  }
1800
2793
  const envExamplePath = path.join(projectPath, ".env.example");
1801
- const envContent = (0, import_agent_runtime6.generateGA4EnvTemplate)();
2794
+ const envContent = (0, import_agent_runtime8.generateGA4EnvTemplate)();
1802
2795
  if (fs.existsSync(envExamplePath)) {
1803
2796
  const existing = fs.readFileSync(envExamplePath, "utf-8");
1804
2797
  if (!existing.includes("GA4_MEASUREMENT_ID")) {
@@ -1813,7 +2806,7 @@ async function setupGA4(projectPath, measurementId) {
1813
2806
  }
1814
2807
  async function setupGSC(projectPath, verificationCode) {
1815
2808
  console.log("Setting up Google Search Console verification...");
1816
- const result = await (0, import_agent_runtime6.injectGSCVerification)(projectPath, verificationCode);
2809
+ const result = await (0, import_agent_runtime8.injectGSCVerification)(projectPath, verificationCode);
1817
2810
  if (result.success) {
1818
2811
  console.log(`\u2705 ${result.message}`);
1819
2812
  if (result.file) {
@@ -1837,7 +2830,7 @@ async function setupGitHubAction(projectPath, schedule, siteUrl) {
1837
2830
  createPRs: true
1838
2831
  }
1839
2832
  };
1840
- const { files, instructions } = (0, import_agent_runtime6.writeGitHubActionFiles)(projectPath, config2);
2833
+ const { files, instructions } = (0, import_agent_runtime8.writeGitHubActionFiles)(projectPath, config2);
1841
2834
  console.log("\u2705 GitHub Action files created:");
1842
2835
  for (const file of files) {
1843
2836
  console.log(` - ${path.relative(projectPath, file)}`);
@@ -1845,34 +2838,199 @@ async function setupGitHubAction(projectPath, schedule, siteUrl) {
1845
2838
  console.log(instructions);
1846
2839
  }
1847
2840
  function showGSCInstructions() {
1848
- console.log((0, import_agent_runtime6.getGSCSetupInstructions)());
2841
+ console.log((0, import_agent_runtime8.getGSCSetupInstructions)());
2842
+ }
2843
+
2844
+ // src/commands/alerts.ts
2845
+ var readline3 = __toESM(require("readline"));
2846
+ var ALERT_TYPES = [
2847
+ { id: "error", label: "Critical errors detected", default: true },
2848
+ { id: "score_drop", label: "Score drops (configurable threshold)", default: true },
2849
+ { id: "weekly_report", label: "Weekly summary reports", default: false },
2850
+ { id: "pr_created", label: "Fix PRs created", default: true }
2851
+ ];
2852
+ async function ask(rl, question) {
2853
+ return new Promise((resolve) => {
2854
+ rl.question(question, (answer) => {
2855
+ resolve(answer.trim());
2856
+ });
2857
+ });
2858
+ }
2859
+ async function askYesNo(rl, question, defaultYes = true) {
2860
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
2861
+ const answer = await ask(rl, `${question} ${suffix}: `);
2862
+ if (answer === "") return defaultYes;
2863
+ return answer.toLowerCase().startsWith("y");
2864
+ }
2865
+ async function alerts(options) {
2866
+ console.log("\n\u{1F514} SEO Autopilot - Alert Configuration\n");
2867
+ const rl = readline3.createInterface({
2868
+ input: process.stdin,
2869
+ output: process.stdout
2870
+ });
2871
+ try {
2872
+ if (options.list) {
2873
+ console.log("\u{1F4CB} Current alert configurations:\n");
2874
+ console.log(" (Alert configs are stored in your RankCLI account)");
2875
+ console.log(" Run `rankcli auth login` first, then manage alerts at https://rankcli.dev/account/alerts\n");
2876
+ rl.close();
2877
+ return;
2878
+ }
2879
+ if (options.test && options.webhook) {
2880
+ await testWebhook(options.channel || "slack", options.webhook);
2881
+ rl.close();
2882
+ return;
2883
+ }
2884
+ console.log("Configure notifications for SEO audit events.\n");
2885
+ console.log("Supported channels:");
2886
+ console.log(" \u2022 Slack - Use an Incoming Webhook URL");
2887
+ console.log(" \u2022 Discord - Use a Channel Webhook URL\n");
2888
+ const channelChoice = await ask(rl, "Which channel? (slack/discord): ");
2889
+ const channel = channelChoice.toLowerCase();
2890
+ if (channel !== "slack" && channel !== "discord") {
2891
+ console.log('\n\u274C Invalid channel. Please choose "slack" or "discord".\n');
2892
+ rl.close();
2893
+ return;
2894
+ }
2895
+ console.log("");
2896
+ if (channel === "slack") {
2897
+ console.log("To create a Slack webhook:");
2898
+ console.log("1. Go to https://api.slack.com/apps");
2899
+ console.log("2. Create an app (or use existing)");
2900
+ console.log('3. Enable "Incoming Webhooks"');
2901
+ console.log("4. Add a new webhook to your workspace");
2902
+ console.log("5. Copy the webhook URL\n");
2903
+ } else {
2904
+ console.log("To create a Discord webhook:");
2905
+ console.log("1. Open Discord channel settings");
2906
+ console.log("2. Go to Integrations \u2192 Webhooks");
2907
+ console.log("3. Create a new webhook");
2908
+ console.log("4. Copy the webhook URL\n");
2909
+ }
2910
+ const webhookUrl = await ask(rl, "Webhook URL: ");
2911
+ if (!webhookUrl.startsWith("https://")) {
2912
+ console.log("\n\u274C Invalid webhook URL. Must start with https://\n");
2913
+ rl.close();
2914
+ return;
2915
+ }
2916
+ console.log("\nWhich events should trigger alerts?\n");
2917
+ const selectedTypes = [];
2918
+ for (const alertType of ALERT_TYPES) {
2919
+ const enabled = await askYesNo(rl, ` ${alertType.label}`, alertType.default);
2920
+ if (enabled) {
2921
+ selectedTypes.push(alertType.id);
2922
+ }
2923
+ }
2924
+ let scoreDropThreshold = 10;
2925
+ if (selectedTypes.includes("score_drop")) {
2926
+ const thresholdStr = await ask(rl, "\nMinimum score drop to alert (default: 10): ");
2927
+ if (thresholdStr) {
2928
+ const parsed = parseInt(thresholdStr, 10);
2929
+ if (!isNaN(parsed) && parsed > 0 && parsed <= 100) {
2930
+ scoreDropThreshold = parsed;
2931
+ }
2932
+ }
2933
+ }
2934
+ console.log("\n\u{1F4E4} Testing webhook...");
2935
+ const testResult = await testWebhook(channel, webhookUrl);
2936
+ if (!testResult) {
2937
+ console.log("\u274C Webhook test failed. Please check the URL and try again.\n");
2938
+ rl.close();
2939
+ return;
2940
+ }
2941
+ console.log("\u2705 Webhook test successful!\n");
2942
+ console.log("\u{1F4DD} Alert configuration:");
2943
+ console.log(` Channel: ${channel}`);
2944
+ console.log(` Events: ${selectedTypes.join(", ")}`);
2945
+ if (selectedTypes.includes("score_drop")) {
2946
+ console.log(` Score drop threshold: ${scoreDropThreshold} points`);
2947
+ }
2948
+ console.log("");
2949
+ console.log("To save this configuration:");
2950
+ console.log("1. Log in with `rankcli auth login`");
2951
+ console.log("2. Add alert config at https://rankcli.dev/account/alerts");
2952
+ console.log("");
2953
+ console.log("Or use the API:");
2954
+ console.log(` POST /api/alerts with webhook_url and settings`);
2955
+ console.log("");
2956
+ rl.close();
2957
+ } catch (error) {
2958
+ console.error("Error:", error);
2959
+ rl.close();
2960
+ process.exit(1);
2961
+ }
2962
+ }
2963
+ async function testWebhook(channel, webhookUrl) {
2964
+ const testPayload = channel === "slack" ? {
2965
+ text: "\u{1F514} RankCLI Alert Test",
2966
+ blocks: [
2967
+ {
2968
+ type: "header",
2969
+ text: {
2970
+ type: "plain_text",
2971
+ text: "\u{1F514} RankCLI Alert Test",
2972
+ emoji: true
2973
+ }
2974
+ },
2975
+ {
2976
+ type: "section",
2977
+ text: {
2978
+ type: "mrkdwn",
2979
+ text: "This is a test message from RankCLI. If you see this, your webhook is working correctly!"
2980
+ }
2981
+ }
2982
+ ]
2983
+ } : {
2984
+ embeds: [
2985
+ {
2986
+ title: "\u{1F514} RankCLI Alert Test",
2987
+ description: "This is a test message from RankCLI. If you see this, your webhook is working correctly!",
2988
+ color: 2450411,
2989
+ footer: {
2990
+ text: "\u{1F916} Sent by RankCLI"
2991
+ },
2992
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2993
+ }
2994
+ ]
2995
+ };
2996
+ try {
2997
+ const response = await fetch(webhookUrl, {
2998
+ method: "POST",
2999
+ headers: { "Content-Type": "application/json" },
3000
+ body: JSON.stringify(testPayload)
3001
+ });
3002
+ return response.ok;
3003
+ } catch (error) {
3004
+ console.error("Webhook test error:", error);
3005
+ return false;
3006
+ }
1849
3007
  }
1850
3008
 
1851
3009
  // src/commands/auth.ts
1852
- var import_chalk3 = __toESM(require("chalk"));
1853
- var import_ora3 = __toESM(require("ora"));
3010
+ var import_chalk6 = __toESM(require("chalk"));
3011
+ var import_ora5 = __toESM(require("ora"));
1854
3012
  var import_inquirer = __toESM(require("inquirer"));
1855
3013
  var import_open = __toESM(require("open"));
1856
3014
  async function login(options) {
1857
3015
  if (isLoggedIn()) {
1858
3016
  const { email } = getUserInfo();
1859
- console.log(import_chalk3.default.yellow(`Already logged in as ${email}`));
1860
- console.log(import_chalk3.default.dim('Run "rankcli logout" to sign out first.'));
3017
+ console.log(import_chalk6.default.yellow(`Already logged in as ${email}`));
3018
+ console.log(import_chalk6.default.dim('Run "rankcli logout" to sign out first.'));
1861
3019
  return;
1862
3020
  }
1863
- const spinner = (0, import_ora3.default)();
3021
+ const spinner = (0, import_ora5.default)();
1864
3022
  try {
1865
3023
  if (options.token) {
1866
3024
  const apiKey = options.token;
1867
3025
  if (!apiKey.startsWith("rankcli_")) {
1868
- console.log(import_chalk3.default.red('Invalid API key format. Keys should start with "rankcli_"'));
3026
+ console.log(import_chalk6.default.red('Invalid API key format. Keys should start with "rankcli_"'));
1869
3027
  return;
1870
3028
  }
1871
3029
  spinner.start("Validating API key...");
1872
3030
  const result = await validateApiKey(apiKey);
1873
3031
  if (!result.valid) {
1874
3032
  spinner.fail("Invalid API key");
1875
- console.log(import_chalk3.default.red(`
3033
+ console.log(import_chalk6.default.red(`
1876
3034
  ${result.error}`));
1877
3035
  return;
1878
3036
  }
@@ -1885,14 +3043,14 @@ ${result.error}`));
1885
3043
  });
1886
3044
  }
1887
3045
  spinner.succeed(`Authenticated via API key`);
1888
- console.log(import_chalk3.default.dim(`
3046
+ console.log(import_chalk6.default.dim(`
1889
3047
  Email: ${result.user?.email}`));
1890
- console.log(import_chalk3.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
3048
+ console.log(import_chalk6.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
1891
3049
  return;
1892
3050
  }
1893
3051
  if (options.browser) {
1894
- console.log(import_chalk3.default.cyan("\nOpening browser for login..."));
1895
- console.log(import_chalk3.default.dim("Complete the login in your browser, then return here.\n"));
3052
+ console.log(import_chalk6.default.cyan("\nOpening browser for login..."));
3053
+ console.log(import_chalk6.default.dim("Complete the login in your browser, then return here.\n"));
1896
3054
  const loginUrl = `${WEB_APP_URL}/login?cli=true`;
1897
3055
  await (0, import_open.default)(loginUrl);
1898
3056
  const { token } = await import_inquirer.default.prompt([
@@ -1908,7 +3066,7 @@ Email: ${result.user?.email}`));
1908
3066
  const { data, error } = await supabase.auth.getUser(token);
1909
3067
  if (error || !data.user) {
1910
3068
  spinner.fail("Invalid token");
1911
- console.log(import_chalk3.default.red("The token is invalid or expired. Please try again."));
3069
+ console.log(import_chalk6.default.red("The token is invalid or expired. Please try again."));
1912
3070
  return;
1913
3071
  }
1914
3072
  const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
@@ -1922,7 +3080,7 @@ Email: ${result.user?.email}`));
1922
3080
  }
1923
3081
  saveSession(sessionData.session);
1924
3082
  await fetchAndSaveSubscription();
1925
- spinner.succeed(`Logged in as ${import_chalk3.default.green(data.user.email)}`);
3083
+ spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user.email)}`);
1926
3084
  } else {
1927
3085
  let email = options.email;
1928
3086
  if (!email) {
@@ -1955,11 +3113,11 @@ Email: ${result.user?.email}`));
1955
3113
  });
1956
3114
  if (error) {
1957
3115
  spinner.fail("Login failed");
1958
- console.log(import_chalk3.default.red(`
3116
+ console.log(import_chalk6.default.red(`
1959
3117
  ${error.message}`));
1960
3118
  if (error.message.includes("Invalid login")) {
1961
- console.log(import_chalk3.default.dim("\nDon't have an account? Sign up at:"));
1962
- console.log(import_chalk3.default.cyan(` ${WEB_APP_URL}/signup`));
3119
+ console.log(import_chalk6.default.dim("\nDon't have an account? Sign up at:"));
3120
+ console.log(import_chalk6.default.cyan(` ${WEB_APP_URL}/signup`));
1963
3121
  }
1964
3122
  return;
1965
3123
  }
@@ -1969,41 +3127,41 @@ ${error.message}`));
1969
3127
  }
1970
3128
  saveSession(data.session);
1971
3129
  await fetchAndSaveSubscription();
1972
- spinner.succeed(`Logged in as ${import_chalk3.default.green(data.user?.email)}`);
3130
+ spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user?.email)}`);
1973
3131
  const { planName, sitesLimit } = getUserInfo();
1974
- console.log(import_chalk3.default.dim(`
3132
+ console.log(import_chalk6.default.dim(`
1975
3133
  Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
1976
3134
  }
1977
3135
  } catch (err) {
1978
3136
  spinner.fail("Login failed");
1979
- console.error(import_chalk3.default.red(err instanceof Error ? err.message : "Unknown error"));
3137
+ console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
1980
3138
  }
1981
3139
  }
1982
3140
  async function logout() {
1983
3141
  if (!isLoggedIn()) {
1984
- console.log(import_chalk3.default.yellow("Not logged in"));
3142
+ console.log(import_chalk6.default.yellow("Not logged in"));
1985
3143
  return;
1986
3144
  }
1987
3145
  const { email } = getUserInfo();
1988
- const spinner = (0, import_ora3.default)("Signing out...").start();
3146
+ const spinner = (0, import_ora5.default)("Signing out...").start();
1989
3147
  try {
1990
3148
  const supabase = getSupabaseClient();
1991
3149
  await supabase.auth.signOut();
1992
3150
  clearCredentials();
1993
- spinner.succeed(`Logged out from ${import_chalk3.default.dim(email)}`);
3151
+ spinner.succeed(`Logged out from ${import_chalk6.default.dim(email)}`);
1994
3152
  } catch (err) {
1995
3153
  spinner.fail("Logout failed");
1996
- console.error(import_chalk3.default.red(err instanceof Error ? err.message : "Unknown error"));
3154
+ console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
1997
3155
  }
1998
3156
  }
1999
3157
  async function whoami() {
2000
3158
  if (!isLoggedIn()) {
2001
- console.log(import_chalk3.default.yellow("Not logged in"));
2002
- console.log(import_chalk3.default.dim('\nRun "rankcli login" to sign in.'));
2003
- console.log(import_chalk3.default.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
3159
+ console.log(import_chalk6.default.yellow("Not logged in"));
3160
+ console.log(import_chalk6.default.dim('\nRun "rankcli login" to sign in.'));
3161
+ console.log(import_chalk6.default.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
2004
3162
  return;
2005
3163
  }
2006
- const spinner = (0, import_ora3.default)("Checking account...").start();
3164
+ const spinner = (0, import_ora5.default)("Checking account...").start();
2007
3165
  try {
2008
3166
  if (isApiKeyAuth()) {
2009
3167
  const apiKey = getApiKey();
@@ -2023,18 +3181,18 @@ async function whoami() {
2023
3181
  const { email, planName, sitesLimit } = getUserInfo();
2024
3182
  const authMethod = isApiKeyAuth() ? "API Key" : "Session";
2025
3183
  spinner.stop();
2026
- console.log(import_chalk3.default.bold("\nRankCLI Account"));
3184
+ console.log(import_chalk6.default.bold("\nRankCLI Account"));
2027
3185
  console.log("\u2500".repeat(40));
2028
- console.log(` Email: ${import_chalk3.default.cyan(email || "N/A")}`);
2029
- console.log(` Plan: ${import_chalk3.default.green(planName)}`);
3186
+ console.log(` Email: ${import_chalk6.default.cyan(email || "N/A")}`);
3187
+ console.log(` Plan: ${import_chalk6.default.green(planName)}`);
2030
3188
  console.log(` Sites: ${sitesLimit} allowed`);
2031
- console.log(` Auth: ${import_chalk3.default.dim(authMethod)}`);
3189
+ console.log(` Auth: ${import_chalk6.default.dim(authMethod)}`);
2032
3190
  console.log("\u2500".repeat(40));
2033
- console.log(import_chalk3.default.dim(`
3191
+ console.log(import_chalk6.default.dim(`
2034
3192
  Manage at: ${WEB_APP_URL}/account`));
2035
3193
  } catch (err) {
2036
3194
  spinner.fail("Failed to fetch account info");
2037
- console.error(import_chalk3.default.red(err instanceof Error ? err.message : "Unknown error"));
3195
+ console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
2038
3196
  }
2039
3197
  }
2040
3198
  async function fetchAndSaveSubscription() {
@@ -2066,7 +3224,7 @@ async function fetchAndSaveSubscription() {
2066
3224
  }
2067
3225
 
2068
3226
  // src/version.ts
2069
- var VERSION = "0.0.18";
3227
+ var VERSION = "0.0.20";
2070
3228
 
2071
3229
  // src/index.ts
2072
3230
  var program = new import_commander.Command();
@@ -2078,6 +3236,20 @@ program.command("init").description("Initialize SEO Autopilot in your project").
2078
3236
  program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
2079
3237
  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);
2080
3238
  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);
3239
+ 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) => {
3240
+ const result = await fix({
3241
+ url: options.url,
3242
+ auto: options.auto,
3243
+ commit: options.commit,
3244
+ push: options.push,
3245
+ dryRun: options.dryRun,
3246
+ only: options.only,
3247
+ output: options.output
3248
+ });
3249
+ if (options.output === "json" && result.json) {
3250
+ console.log(result.json);
3251
+ }
3252
+ });
2081
3253
  program.command("geo").description("GEO analysis - Check AI search visibility (ChatGPT, Perplexity, Claude)").option("-u, --url <url>", "URL to analyze").option("-o, --output <format>", "Output format (json, console)", "console").action(geo);
2082
3254
  program.command("keywords").description("AI-powered keyword research with actionable suggestions").option("-u, --url <url>", "Your website URL").option("-s, --seed <keywords>", "Seed keywords (comma-separated)").option("--auto", "Auto-extract seed keywords from your page").option("--quick", "Quick mode - skip questions, use defaults").option("--ai", "Use GPT-4 for enhanced analysis with free tool ideas").option("--local", "Force local processing (skip cloud worker)").option("--competitor", "Competitor gap analysis mode (SpyFu Kombat-style)").option("-c, --competitors <domains>", "Competitor domains (comma-separated)").action(async (options) => {
2083
3255
  if (options.ai && options.url) {
@@ -2100,8 +3272,42 @@ program.command("intent").description("Classify search intent for a keyword").ar
2100
3272
  program.command("competitors").description("Analyze competitor SEO strategies").option("-d, --domain <domain>", "Competitor domain to analyze").action(() => {
2101
3273
  console.log("Competitor analysis coming soon...");
2102
3274
  });
2103
- program.command("rank").description("Check current keyword rankings").action(() => {
2104
- console.log("Rank tracking coming soon...");
3275
+ var trackCmd = program.command("track").description("Keyword rank tracking");
3276
+ trackCmd.command("add <keywords...>").description("Add keywords to track").option("-u, --url <url>", "URL to track rankings for").option("-e, --engine <engine>", "Search engine (google, bing)", "google").option("-c, --country <country>", "Country code (US, UK, DE, etc.)", "US").option("-l, --language <lang>", "Language code (en, de, fr, etc.)", "en").action(async (keywords2, options) => {
3277
+ await trackAdd({
3278
+ keywords: keywords2,
3279
+ url: options.url,
3280
+ searchEngine: options.engine,
3281
+ country: options.country,
3282
+ language: options.language
3283
+ });
3284
+ });
3285
+ trackCmd.command("status").description("Show current keyword rankings").option("-u, --url <url>", "URL to show rankings for").option("-o, --output <format>", "Output format (text, json)", "text").action(async (options) => {
3286
+ await trackStatus({
3287
+ url: options.url,
3288
+ output: options.output
3289
+ });
3290
+ });
3291
+ trackCmd.command("history").description("Show keyword position history").option("-u, --url <url>", "URL to show history for").option("-k, --keyword <keyword>", "Keyword to show history for").option("-d, --days <days>", "Number of days of history", "30").option("-o, --output <format>", "Output format (text, json)", "text").action(async (options) => {
3292
+ await trackHistory({
3293
+ url: options.url,
3294
+ keyword: options.keyword,
3295
+ days: parseInt(options.days, 10),
3296
+ output: options.output
3297
+ });
3298
+ });
3299
+ trackCmd.command("check").description("Check current rankings (calls SERP API)").option("-u, --url <url>", "URL to check rankings for").option("-f, --force", "Force check even if rate limited").action(async (options) => {
3300
+ await trackCheck({
3301
+ url: options.url,
3302
+ force: options.force
3303
+ });
3304
+ });
3305
+ trackCmd.command("export").description("Export tracking data").option("-u, --url <url>", "URL to export data for").option("-f, --format <format>", "Export format (csv, json)", "csv").option("-d, --days <days>", "Number of days of history", "30").action(async (options) => {
3306
+ await trackExport({
3307
+ url: options.url,
3308
+ format: options.format,
3309
+ days: parseInt(options.days, 10)
3310
+ });
2105
3311
  });
2106
3312
  program.command("report").description("Generate SEO performance report").option("-f, --format <format>", "Report format (pdf, html, json)", "html").action(() => {
2107
3313
  console.log("Report generation coming soon...");
@@ -2111,4 +3317,5 @@ program.command("watch").description("Continuous monitoring mode").action(() =>
2111
3317
  });
2112
3318
  program.command("setup").description("Set up SEO tracking (GA4, Search Console, GitHub Actions)").option("--ga4 <id>", "Google Analytics 4 Measurement ID (G-XXXXXXXXXX)").option("--gsc <code>", "Google Search Console verification code").option("--github-action", "Set up automated GitHub Action").option("--schedule <freq>", "GitHub Action schedule (daily, weekly, monthly)", "weekly").option("--all", "Interactive setup wizard").action(setup);
2113
3319
  program.command("gsc-setup").description("Show Google Search Console API setup instructions").action(showGSCInstructions);
3320
+ program.command("alerts").description("Configure Slack/Discord notifications for SEO events").option("-c, --channel <channel>", "Alert channel (slack, discord)").option("-w, --webhook <url>", "Webhook URL").option("--test", "Test the webhook").option("--list", "List current alert configurations").option("--remove <id>", "Remove an alert configuration").action(alerts);
2114
3321
  program.parse();