@rankcli/cli 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -592,6 +592,23 @@ var UPSELL_MESSAGES = {
592
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
593
  ""
594
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
+ ],
595
612
  // Rate limit exceeded
596
613
  rate_limit: (ctx) => [
597
614
  "",
@@ -783,7 +800,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
783
800
  onPage: report.healthScore?.onPage || 0,
784
801
  content: report.healthScore?.content || 0,
785
802
  performance: report.healthScore?.performance || 0,
786
- security: report.healthScore?.security || 0
803
+ security: report.healthScore?.security || 0,
804
+ localSeo: report.healthScore?.localSeo || 0,
805
+ aiReadiness: report.healthScore?.aiReadiness || 0
787
806
  };
788
807
  const now = (/* @__PURE__ */ new Date()).toISOString();
789
808
  const { data: audit2, error: auditError } = await supabase.from("audits").insert({
@@ -847,7 +866,7 @@ async function audit(options) {
847
866
  const maxPages = parseInt(options.maxPages || "5", 10);
848
867
  const enableAI = options.ai || false;
849
868
  const aiProvider = options.aiProvider || "openai";
850
- 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) || "";
851
870
  const loggedIn = isLoggedIn();
852
871
  if (!url) {
853
872
  console.log("\n\u26A0\uFE0F Please provide a URL to audit:");
@@ -1000,6 +1019,8 @@ async function audit(options) {
1000
1019
  console.log(` Content: ${bar(hs.content || 0)} ${hs.content || 0}%`);
1001
1020
  console.log(` Performance: ${bar(hs.performance || 0)} ${hs.performance || 0}%`);
1002
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}%`);
1003
1024
  }
1004
1025
  const errors = mergedIssues.filter((i) => i.severity === "error");
1005
1026
  const warnings = mergedIssues.filter((i) => i.severity === "warning");
@@ -1026,9 +1047,9 @@ async function audit(options) {
1026
1047
  });
1027
1048
  if (aiAnalysis.prioritizedFixes?.length > 0) {
1028
1049
  console.log("\n " + import_chalk2.default.bold("Priority Fixes:"));
1029
- aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix, i) => {
1030
- console.log(` ${i + 1}. [P${fix.priority}] ${fix.code}`);
1031
- console.log(import_chalk2.default.dim(` ${fix.rationale}`));
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}`));
1032
1053
  });
1033
1054
  }
1034
1055
  }
@@ -1079,19 +1100,19 @@ var SEVERITY_ICON = {
1079
1100
  warning: "\u26A0\uFE0F",
1080
1101
  info: "\u2139\uFE0F"
1081
1102
  };
1082
- function printFix(fix, index) {
1083
- const icon = SEVERITY_ICON[fix.issue.severity] || "\u2022";
1103
+ function printFix(fix2, index) {
1104
+ const icon = SEVERITY_ICON[fix2.issue.severity] || "\u2022";
1084
1105
  console.log(`
1085
- ${index + 1}. ${icon} ${fix.issue.message}`);
1086
- console.log(` File: ${fix.file}`);
1087
- if (fix.skipped) {
1088
- 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}`);
1089
1110
  } else {
1090
- console.log(` Explanation: ${fix.explanation}`);
1091
- if (fix.before) {
1092
- 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)}`);
1093
1114
  }
1094
- console.log(` After: ${truncate(fix.after, 100)}`);
1115
+ console.log(` After: ${truncate(fix2.after, 100)}`);
1095
1116
  }
1096
1117
  }
1097
1118
  function truncate(str, maxLen) {
@@ -1217,15 +1238,15 @@ async function apply(options) {
1217
1238
  if (applied.length > 0) {
1218
1239
  console.log(`
1219
1240
  \u2705 Applied ${applied.length} fix(es):`);
1220
- for (const fix of applied) {
1221
- console.log(` - ${fix.file}: ${fix.issue.message}`);
1241
+ for (const fix2 of applied) {
1242
+ console.log(` - ${fix2.file}: ${fix2.issue.message}`);
1222
1243
  }
1223
1244
  }
1224
1245
  if (skipped.length > 0) {
1225
1246
  console.log(`
1226
1247
  \u26A0\uFE0F Skipped ${skipped.length} fix(es):`);
1227
- for (const fix of skipped) {
1228
- 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"}`);
1229
1250
  }
1230
1251
  }
1231
1252
  if (config2 && !config2.url) {
@@ -1245,10 +1266,788 @@ async function apply(options) {
1245
1266
  console.log("");
1246
1267
  }
1247
1268
 
1248
- // src/commands/geo.ts
1269
+ // src/commands/fix.ts
1270
+ var import_child_process = require("child_process");
1271
+ var import_util = require("util");
1249
1272
  var import_agent_runtime3 = require("@rankcli/agent-runtime");
1250
1273
  var import_ora2 = __toESM(require("ora"));
1251
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"));
1252
2051
  async function fetchResources(url) {
1253
2052
  const headers = {};
1254
2053
  let html = null;
@@ -1294,7 +2093,7 @@ async function geo(options) {
1294
2093
  console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
1295
2094
  return;
1296
2095
  }
1297
- 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();
1298
2097
  const { html, robotsTxt, headers } = await fetchResources(url);
1299
2098
  if (!html) {
1300
2099
  spinner.fail("Could not fetch page content");
@@ -1302,11 +2101,11 @@ async function geo(options) {
1302
2101
  }
1303
2102
  spinner.text = "Running GEO analysis...";
1304
2103
  try {
1305
- const geoResult = await import_agent_runtime3.analyzers.analyzeGEO(html, url, robotsTxt || void 0);
1306
- const cwvResult = import_agent_runtime3.analyzers.analyzeCoreWebVitals(html, url, headers);
1307
- const securityResult = import_agent_runtime3.analyzers.analyzeSecurityHeaders(headers, url);
1308
- const schemaResult = import_agent_runtime3.analyzers.analyzeStructuredData(html, url);
1309
- 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);
1310
2109
  spinner.succeed("GEO analysis complete");
1311
2110
  if (options.output === "json") {
1312
2111
  const jsonOutput = {
@@ -1320,55 +2119,55 @@ async function geo(options) {
1320
2119
  console.log(JSON.stringify(jsonOutput, null, 2));
1321
2120
  return;
1322
2121
  }
1323
- console.log("\n" + import_chalk3.default.bold("\u2550".repeat(60)));
1324
- console.log(import_chalk3.default.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
1325
- console.log(import_chalk3.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)));
1326
2125
  const geoScore = geoResult.score;
1327
- const scoreColor = geoScore >= 80 ? import_chalk3.default.green : geoScore >= 60 ? import_chalk3.default.yellow : import_chalk3.default.red;
2126
+ const scoreColor = geoScore >= 80 ? import_chalk5.default.green : geoScore >= 60 ? import_chalk5.default.yellow : import_chalk5.default.red;
1328
2127
  console.log(`
1329
- ${import_chalk3.default.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
1330
- console.log("\n" + import_chalk3.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:"));
1331
2130
  const { aiCrawlerAccess } = geoResult;
1332
2131
  if (aiCrawlerAccess.blockedCrawlers.length > 0) {
1333
- console.log(import_chalk3.default.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
2132
+ console.log(import_chalk5.default.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
1334
2133
  }
1335
2134
  if (aiCrawlerAccess.allowedCrawlers.length > 0) {
1336
2135
  const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
1337
- console.log(import_chalk3.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 ? "..." : ""}`));
1338
2137
  }
1339
- console.log("\n" + import_chalk3.default.bold(" Rendering:"));
2138
+ console.log("\n" + import_chalk5.default.bold(" Rendering:"));
1340
2139
  if (aiCrawlerAccess.serverSideRendered) {
1341
- console.log(import_chalk3.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)"));
1342
2141
  } else if (aiCrawlerAccess.jsRenderingRequired) {
1343
- console.log(import_chalk3.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)"));
1344
2143
  }
1345
- console.log("\n" + import_chalk3.default.bold(" LLM Friendliness:"));
2144
+ console.log("\n" + import_chalk5.default.bold(" LLM Friendliness:"));
1346
2145
  const { llmSignals } = geoResult;
1347
2146
  const bar = (score) => {
1348
2147
  const filled = Math.round(score / 5);
1349
2148
  const empty = 20 - filled;
1350
- const color = score >= 70 ? import_chalk3.default.green : score >= 40 ? import_chalk3.default.yellow : import_chalk3.default.red;
1351
- return color("\u2588".repeat(filled)) + import_chalk3.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));
1352
2151
  };
1353
2152
  console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
1354
2153
  console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
1355
2154
  console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
1356
2155
  console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
1357
- console.log("\n" + import_chalk3.default.bold(" Content Structure:"));
2156
+ console.log("\n" + import_chalk5.default.bold(" Content Structure:"));
1358
2157
  const { contentStructure } = geoResult;
1359
- console.log(` ${contentStructure.hasStructuredData ? import_chalk3.default.green("\u2705") : import_chalk3.default.red("\u274C")} JSON-LD Structured Data`);
1360
- console.log(` ${contentStructure.hasFAQSchema ? import_chalk3.default.green("\u2705") : import_chalk3.default.yellow("\u25CB")} FAQ Schema`);
1361
- console.log(` ${contentStructure.hasArticleSchema ? import_chalk3.default.green("\u2705") : import_chalk3.default.yellow("\u25CB")} Article Schema`);
1362
- console.log(` ${contentStructure.hasBreadcrumbs ? import_chalk3.default.green("\u2705") : import_chalk3.default.yellow("\u25CB")} Breadcrumbs`);
1363
- console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? import_chalk3.default.green("Good") : contentStructure.headingHierarchy === "needs-work" ? import_chalk3.default.yellow("Needs Work") : import_chalk3.default.red("Poor")}`);
1364
- console.log("\n" + import_chalk3.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:"));
1365
2164
  const { citationReadiness } = geoResult;
1366
2165
  if (citationReadiness.trustSignals.length > 0) {
1367
- console.log(import_chalk3.default.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
2166
+ console.log(import_chalk5.default.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
1368
2167
  } else {
1369
- console.log(import_chalk3.default.yellow(" \u25CB No trust signals detected"));
2168
+ console.log(import_chalk5.default.yellow(" \u25CB No trust signals detected"));
1370
2169
  }
1371
- console.log("\n" + import_chalk3.default.bold(" Other Scores:"));
2170
+ console.log("\n" + import_chalk5.default.bold(" Other Scores:"));
1372
2171
  console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
1373
2172
  console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
1374
2173
  console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
@@ -1382,18 +2181,18 @@ async function geo(options) {
1382
2181
  ];
1383
2182
  const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
1384
2183
  if (criticalIssues.length > 0) {
1385
- console.log("\n" + import_chalk3.default.bold.red(" Critical Issues:"));
2184
+ console.log("\n" + import_chalk5.default.bold.red(" Critical Issues:"));
1386
2185
  criticalIssues.slice(0, 5).forEach((issue, i) => {
1387
- console.log(import_chalk3.default.red(` ${i + 1}. ${issue.title}`));
2186
+ console.log(import_chalk5.default.red(` ${i + 1}. ${issue.title}`));
1388
2187
  });
1389
2188
  }
1390
2189
  if (geoResult.recommendations.length > 0) {
1391
- console.log("\n" + import_chalk3.default.bold(" Recommendations:"));
2190
+ console.log("\n" + import_chalk5.default.bold(" Recommendations:"));
1392
2191
  geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
1393
2192
  console.log(` ${i + 1}. ${rec}`);
1394
2193
  });
1395
2194
  }
1396
- console.log("\n" + import_chalk3.default.bold("\u2550".repeat(60)));
2195
+ console.log("\n" + import_chalk5.default.bold("\u2550".repeat(60)));
1397
2196
  console.log("");
1398
2197
  } catch (error) {
1399
2198
  spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1402,7 +2201,7 @@ async function geo(options) {
1402
2201
 
1403
2202
  // src/commands/keywords.ts
1404
2203
  var readline = __toESM(require("readline"));
1405
- var import_agent_runtime4 = require("@rankcli/agent-runtime");
2204
+ var import_agent_runtime6 = require("@rankcli/agent-runtime");
1406
2205
  async function keywords(options) {
1407
2206
  console.log("\n\u{1F511} SEO Autopilot - Keyword Research\n");
1408
2207
  console.log("This tool helps you find keywords you can actually rank for,");
@@ -1411,7 +2210,7 @@ async function keywords(options) {
1411
2210
  input: process.stdin,
1412
2211
  output: process.stdout
1413
2212
  });
1414
- const ask = (question) => {
2213
+ const ask2 = (question) => {
1415
2214
  return new Promise((resolve) => {
1416
2215
  rl.question(question, (answer) => {
1417
2216
  resolve(answer.trim());
@@ -1426,7 +2225,7 @@ ${question}`);
1426
2225
  console.log(` ${choice.description}`);
1427
2226
  });
1428
2227
  while (true) {
1429
- const answer = await ask("\nEnter your choice (1-" + choices.length + "): ");
2228
+ const answer = await ask2("\nEnter your choice (1-" + choices.length + "): ");
1430
2229
  const num = parseInt(answer, 10);
1431
2230
  if (num >= 1 && num <= choices.length) {
1432
2231
  return choices[num - 1].value;
@@ -1437,7 +2236,7 @@ ${question}`);
1437
2236
  try {
1438
2237
  let url = options.url;
1439
2238
  if (!url) {
1440
- url = await ask("Enter your website URL: ");
2239
+ url = await ask2("Enter your website URL: ");
1441
2240
  }
1442
2241
  try {
1443
2242
  const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
@@ -1453,7 +2252,7 @@ ${question}`);
1453
2252
  seedKeywords = options.seed.split(",").map((s) => s.trim());
1454
2253
  } else if (options.auto) {
1455
2254
  console.log("\n\u{1F50D} Auto-extracting seed keywords from your page...");
1456
- seedKeywords = await (0, import_agent_runtime4.extractSeedKeywords)(url);
2255
+ seedKeywords = await (0, import_agent_runtime6.extractSeedKeywords)(url);
1457
2256
  if (seedKeywords.length > 0) {
1458
2257
  console.log(`Found: ${seedKeywords.join(", ")}`);
1459
2258
  }
@@ -1461,7 +2260,7 @@ ${question}`);
1461
2260
  if (seedKeywords.length === 0) {
1462
2261
  console.log("\n\u{1F4DD} What is your website/product about?");
1463
2262
  console.log(" (Enter 1-3 main topics or keywords, comma-separated)");
1464
- const seedInput = await ask(" > ");
2263
+ const seedInput = await ask2(" > ");
1465
2264
  seedKeywords = seedInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1466
2265
  if (seedKeywords.length === 0) {
1467
2266
  console.log("\n\u274C Please provide at least one seed keyword.\n");
@@ -1478,7 +2277,7 @@ ${question}`);
1478
2277
  contentCapacity: "medium",
1479
2278
  targetGeo: "us"
1480
2279
  };
1481
- for (const q of import_agent_runtime4.SITE_PROFILE_QUESTIONS) {
2280
+ for (const q of import_agent_runtime6.SITE_PROFILE_QUESTIONS) {
1482
2281
  const answer = await askChoice(q.question, q.options);
1483
2282
  siteProfile[q.id] = answer;
1484
2283
  }
@@ -1497,13 +2296,13 @@ ${question}`);
1497
2296
  url,
1498
2297
  maxKeywords: 100
1499
2298
  };
1500
- const result = await (0, import_agent_runtime4.runKeywordResearch)(researchOptions);
1501
- 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));
1502
2301
  const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
1503
2302
  if (allKeywords.length > 5) {
1504
2303
  console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
1505
- const topicResult = (0, import_agent_runtime4.groupKeywordsByTopic)(allKeywords);
1506
- 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));
1507
2306
  }
1508
2307
  if (result.quickWins.length > 0) {
1509
2308
  console.log('\n\u{1F4A1} TIP: Run "seo apply --url ' + url + '" to apply keyword optimizations');
@@ -1528,7 +2327,7 @@ async function keywordsQuick(url, providedSeeds) {
1528
2327
  `);
1529
2328
  } else {
1530
2329
  console.log("\u{1F50D} Extracting keywords from your page...");
1531
- seedKeywords = await (0, import_agent_runtime4.extractSeedKeywords)(url);
2330
+ seedKeywords = await (0, import_agent_runtime6.extractSeedKeywords)(url);
1532
2331
  if (seedKeywords.length === 0) {
1533
2332
  console.log("\u274C Could not extract keywords. Please use interactive mode.\n");
1534
2333
  return;
@@ -1544,18 +2343,18 @@ async function keywordsQuick(url, providedSeeds) {
1544
2343
  contentCapacity: "medium",
1545
2344
  targetGeo: "us"
1546
2345
  };
1547
- const result = await (0, import_agent_runtime4.runKeywordResearch)({
2346
+ const result = await (0, import_agent_runtime6.runKeywordResearch)({
1548
2347
  seedKeywords,
1549
2348
  siteProfile,
1550
2349
  url,
1551
2350
  maxKeywords: 50
1552
2351
  });
1553
- console.log((0, import_agent_runtime4.formatKeywordReport)(result));
2352
+ console.log((0, import_agent_runtime6.formatKeywordReport)(result));
1554
2353
  const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
1555
2354
  if (allKeywords.length > 5) {
1556
2355
  console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
1557
- const topicResult = (0, import_agent_runtime4.groupKeywordsByTopic)(allKeywords);
1558
- 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));
1559
2358
  }
1560
2359
  } catch (error) {
1561
2360
  console.log(`
@@ -1618,7 +2417,7 @@ ${result2.teaser.upgradeMessage}`);
1618
2417
  return;
1619
2418
  }
1620
2419
  console.log("\u{1F52C} Running local AI analysis (this may take a minute)...\n");
1621
- const result = await (0, import_agent_runtime4.runAIKeywordResearch)({
2420
+ const result = await (0, import_agent_runtime6.runAIKeywordResearch)({
1622
2421
  url,
1623
2422
  tier: "solo",
1624
2423
  openaiApiKey,
@@ -1737,12 +2536,12 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
1737
2536
  }
1738
2537
  console.log("");
1739
2538
  console.log("Analyzing competitor keywords (this may take a moment)...\n");
1740
- const result = await (0, import_agent_runtime4.discoverCompetitorKeywords)(
2539
+ const result = await (0, import_agent_runtime6.discoverCompetitorKeywords)(
1741
2540
  yourDomain,
1742
2541
  seedKeywords,
1743
2542
  competitors
1744
2543
  );
1745
- console.log((0, import_agent_runtime4.formatCompetitorReport)(result));
2544
+ console.log((0, import_agent_runtime6.formatCompetitorReport)(result));
1746
2545
  if (result.missingKeywords.length > 0) {
1747
2546
  console.log("\n\u{1F3AF} ACTION ITEMS:");
1748
2547
  console.log("\u2500".repeat(60));
@@ -1761,7 +2560,7 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
1761
2560
  }
1762
2561
 
1763
2562
  // src/commands/content.ts
1764
- var import_agent_runtime5 = require("@rankcli/agent-runtime");
2563
+ var import_agent_runtime7 = require("@rankcli/agent-runtime");
1765
2564
  async function analyzeContent(options) {
1766
2565
  console.log("\n\u{1F4DD} SEO Autopilot - Content Analysis\n");
1767
2566
  const mode = options.mode || "full";
@@ -1771,12 +2570,12 @@ async function analyzeContent(options) {
1771
2570
  console.log('\u274C Please provide a headline with --headline "Your Headline Here"');
1772
2571
  return;
1773
2572
  }
1774
- const analysis = (0, import_agent_runtime5.analyzeHeadline)(headline);
1775
- 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));
1776
2575
  if (options.keyword) {
1777
2576
  console.log("\n\u{1F4CB} HEADLINE VARIATIONS");
1778
2577
  console.log("\u2500".repeat(60));
1779
- const variations = (0, import_agent_runtime5.generateHeadlineVariations)(options.keyword, [options.keyword]);
2578
+ const variations = (0, import_agent_runtime7.generateHeadlineVariations)(options.keyword, [options.keyword]);
1780
2579
  for (const v of variations.slice(0, 5)) {
1781
2580
  console.log(` \u2022 ${v}`);
1782
2581
  }
@@ -1787,7 +2586,7 @@ async function analyzeContent(options) {
1787
2586
  if (options.url) {
1788
2587
  console.log(`Fetching content from ${options.url}...`);
1789
2588
  try {
1790
- const result = await (0, import_agent_runtime5.crawlUrl)(options.url);
2589
+ const result = await (0, import_agent_runtime7.crawlUrl)(options.url);
1791
2590
  if (!result.html) {
1792
2591
  console.log("\u274C Could not fetch content from URL");
1793
2592
  return;
@@ -1801,15 +2600,15 @@ async function analyzeContent(options) {
1801
2600
  Analyzing: "${title || options.url}"
1802
2601
  `);
1803
2602
  if (mode === "full" || mode === "readability") {
1804
- const readability = (0, import_agent_runtime5.analyzeReadability)(bodyText);
1805
- 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));
1806
2605
  }
1807
2606
  if (mode === "full" && title) {
1808
- const headlineAnalysis = (0, import_agent_runtime5.analyzeHeadline)(title);
1809
- 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));
1810
2609
  }
1811
2610
  if (options.keyword && (mode === "full" || mode === "density")) {
1812
- const densityAnalysis = (0, import_agent_runtime5.analyzeKeywordDensity)(options.keyword, {
2611
+ const densityAnalysis = (0, import_agent_runtime7.analyzeKeywordDensity)(options.keyword, {
1813
2612
  title,
1814
2613
  h1,
1815
2614
  headings,
@@ -1817,19 +2616,19 @@ Analyzing: "${title || options.url}"
1817
2616
  firstParagraph: paragraphs[0],
1818
2617
  lastParagraph: paragraphs[paragraphs.length - 1]
1819
2618
  });
1820
- console.log((0, import_agent_runtime5.formatKeywordDensityReport)(densityAnalysis));
2619
+ console.log((0, import_agent_runtime7.formatKeywordDensityReport)(densityAnalysis));
1821
2620
  }
1822
2621
  if (options.keyword && (mode === "full" || mode === "snippet")) {
1823
- const snippetAnalysis = (0, import_agent_runtime5.analyzeFeaturedSnippetPotential)(
2622
+ const snippetAnalysis = (0, import_agent_runtime7.analyzeFeaturedSnippetPotential)(
1824
2623
  options.keyword,
1825
2624
  bodyText,
1826
2625
  headings
1827
2626
  );
1828
- console.log((0, import_agent_runtime5.formatFeaturedSnippetReport)(snippetAnalysis));
2627
+ console.log((0, import_agent_runtime7.formatFeaturedSnippetReport)(snippetAnalysis));
1829
2628
  }
1830
2629
  if (options.keyword) {
1831
- const intentAnalysis = (0, import_agent_runtime5.classifyIntent)(options.keyword);
1832
- 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));
1833
2632
  }
1834
2633
  console.log("\n\u{1F4CA} CONTENT SUMMARY");
1835
2634
  console.log("\u2500".repeat(60));
@@ -1856,8 +2655,8 @@ Analyzing: "${title || options.url}"
1856
2655
  }
1857
2656
  async function analyzeIntent(keyword) {
1858
2657
  console.log("\n\u{1F50D} Search Intent Analysis\n");
1859
- const analysis = (0, import_agent_runtime5.classifyIntent)(keyword);
1860
- 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));
1861
2660
  }
1862
2661
  function extractBodyText(html) {
1863
2662
  let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
@@ -1876,7 +2675,7 @@ function extractBodyText(html) {
1876
2675
  var readline2 = __toESM(require("readline"));
1877
2676
  var fs = __toESM(require("fs"));
1878
2677
  var path = __toESM(require("path"));
1879
- var import_agent_runtime6 = require("@rankcli/agent-runtime");
2678
+ var import_agent_runtime8 = require("@rankcli/agent-runtime");
1880
2679
  async function setup(options) {
1881
2680
  console.log("\n\u{1F527} SEO Autopilot - Setup\n");
1882
2681
  const projectPath = process.cwd();
@@ -1902,7 +2701,7 @@ async function interactiveSetup(projectPath) {
1902
2701
  input: process.stdin,
1903
2702
  output: process.stdout
1904
2703
  });
1905
- const ask = (question) => {
2704
+ const ask2 = (question) => {
1906
2705
  return new Promise((resolve) => {
1907
2706
  rl.question(question, (answer) => {
1908
2707
  resolve(answer.trim());
@@ -1916,7 +2715,7 @@ async function interactiveSetup(projectPath) {
1916
2715
  console.log("GA4 tracks visitor behavior on your site.");
1917
2716
  console.log("Get your Measurement ID from: https://analytics.google.com");
1918
2717
  console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
1919
- 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: ");
1920
2719
  if (ga4Id && ga4Id.startsWith("G-")) {
1921
2720
  await setupGA4(projectPath, ga4Id);
1922
2721
  } else if (ga4Id) {
@@ -1929,7 +2728,7 @@ async function interactiveSetup(projectPath) {
1929
2728
  console.log("GSC shows how Google sees your site and what queries you rank for.");
1930
2729
  console.log("Get your verification code from: https://search.google.com/search-console");
1931
2730
  console.log("Go to: Settings > Ownership verification > HTML tag\n");
1932
- 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: ");
1933
2732
  if (gscCode && gscCode.length > 10) {
1934
2733
  await setupGSC(projectPath, gscCode);
1935
2734
  } else if (gscCode) {
@@ -1941,20 +2740,20 @@ async function interactiveSetup(projectPath) {
1941
2740
  console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
1942
2741
  console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
1943
2742
  console.log("It creates issues with reports and can auto-fix some problems.\n");
1944
- const setupAction = await ask("Set up GitHub Action? (y/n): ");
2743
+ const setupAction = await ask2("Set up GitHub Action? (y/n): ");
1945
2744
  if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
1946
2745
  console.log("\nHow often should the check run?");
1947
2746
  console.log(" 1. Weekly (recommended for most sites)");
1948
2747
  console.log(" 2. Daily (for high-traffic sites)");
1949
2748
  console.log(" 3. Monthly (for low-traffic sites)\n");
1950
- const scheduleChoice = await ask("Enter choice (1-3): ");
2749
+ const scheduleChoice = await ask2("Enter choice (1-3): ");
1951
2750
  const scheduleMap = {
1952
2751
  "1": "weekly",
1953
2752
  "2": "daily",
1954
2753
  "3": "monthly"
1955
2754
  };
1956
2755
  const schedule = scheduleMap[scheduleChoice] || "weekly";
1957
- const siteUrl = await ask("Enter your site URL (https://example.com): ");
2756
+ const siteUrl = await ask2("Enter your site URL (https://example.com): ");
1958
2757
  if (siteUrl) {
1959
2758
  await setupGitHubAction(projectPath, schedule, siteUrl);
1960
2759
  }
@@ -1978,7 +2777,7 @@ async function interactiveSetup(projectPath) {
1978
2777
  }
1979
2778
  async function setupGA4(projectPath, measurementId) {
1980
2779
  console.log(`Setting up Google Analytics 4 (${measurementId})...`);
1981
- const result = await (0, import_agent_runtime6.injectGA4)(projectPath, { measurementId });
2780
+ const result = await (0, import_agent_runtime8.injectGA4)(projectPath, { measurementId });
1982
2781
  if (result.success) {
1983
2782
  console.log(`\u2705 ${result.message}`);
1984
2783
  if (result.file) {
@@ -1992,7 +2791,7 @@ async function setupGA4(projectPath, measurementId) {
1992
2791
  }
1993
2792
  }
1994
2793
  const envExamplePath = path.join(projectPath, ".env.example");
1995
- const envContent = (0, import_agent_runtime6.generateGA4EnvTemplate)();
2794
+ const envContent = (0, import_agent_runtime8.generateGA4EnvTemplate)();
1996
2795
  if (fs.existsSync(envExamplePath)) {
1997
2796
  const existing = fs.readFileSync(envExamplePath, "utf-8");
1998
2797
  if (!existing.includes("GA4_MEASUREMENT_ID")) {
@@ -2007,7 +2806,7 @@ async function setupGA4(projectPath, measurementId) {
2007
2806
  }
2008
2807
  async function setupGSC(projectPath, verificationCode) {
2009
2808
  console.log("Setting up Google Search Console verification...");
2010
- const result = await (0, import_agent_runtime6.injectGSCVerification)(projectPath, verificationCode);
2809
+ const result = await (0, import_agent_runtime8.injectGSCVerification)(projectPath, verificationCode);
2011
2810
  if (result.success) {
2012
2811
  console.log(`\u2705 ${result.message}`);
2013
2812
  if (result.file) {
@@ -2031,7 +2830,7 @@ async function setupGitHubAction(projectPath, schedule, siteUrl) {
2031
2830
  createPRs: true
2032
2831
  }
2033
2832
  };
2034
- const { files, instructions } = (0, import_agent_runtime6.writeGitHubActionFiles)(projectPath, config2);
2833
+ const { files, instructions } = (0, import_agent_runtime8.writeGitHubActionFiles)(projectPath, config2);
2035
2834
  console.log("\u2705 GitHub Action files created:");
2036
2835
  for (const file of files) {
2037
2836
  console.log(` - ${path.relative(projectPath, file)}`);
@@ -2039,34 +2838,199 @@ async function setupGitHubAction(projectPath, schedule, siteUrl) {
2039
2838
  console.log(instructions);
2040
2839
  }
2041
2840
  function showGSCInstructions() {
2042
- 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
+ }
2043
3007
  }
2044
3008
 
2045
3009
  // src/commands/auth.ts
2046
- var import_chalk4 = __toESM(require("chalk"));
2047
- var import_ora3 = __toESM(require("ora"));
3010
+ var import_chalk6 = __toESM(require("chalk"));
3011
+ var import_ora5 = __toESM(require("ora"));
2048
3012
  var import_inquirer = __toESM(require("inquirer"));
2049
3013
  var import_open = __toESM(require("open"));
2050
3014
  async function login(options) {
2051
3015
  if (isLoggedIn()) {
2052
3016
  const { email } = getUserInfo();
2053
- console.log(import_chalk4.default.yellow(`Already logged in as ${email}`));
2054
- console.log(import_chalk4.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.'));
2055
3019
  return;
2056
3020
  }
2057
- const spinner = (0, import_ora3.default)();
3021
+ const spinner = (0, import_ora5.default)();
2058
3022
  try {
2059
3023
  if (options.token) {
2060
3024
  const apiKey = options.token;
2061
3025
  if (!apiKey.startsWith("rankcli_")) {
2062
- console.log(import_chalk4.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_"'));
2063
3027
  return;
2064
3028
  }
2065
3029
  spinner.start("Validating API key...");
2066
3030
  const result = await validateApiKey(apiKey);
2067
3031
  if (!result.valid) {
2068
3032
  spinner.fail("Invalid API key");
2069
- console.log(import_chalk4.default.red(`
3033
+ console.log(import_chalk6.default.red(`
2070
3034
  ${result.error}`));
2071
3035
  return;
2072
3036
  }
@@ -2079,14 +3043,14 @@ ${result.error}`));
2079
3043
  });
2080
3044
  }
2081
3045
  spinner.succeed(`Authenticated via API key`);
2082
- console.log(import_chalk4.default.dim(`
3046
+ console.log(import_chalk6.default.dim(`
2083
3047
  Email: ${result.user?.email}`));
2084
- console.log(import_chalk4.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
3048
+ console.log(import_chalk6.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
2085
3049
  return;
2086
3050
  }
2087
3051
  if (options.browser) {
2088
- console.log(import_chalk4.default.cyan("\nOpening browser for login..."));
2089
- console.log(import_chalk4.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"));
2090
3054
  const loginUrl = `${WEB_APP_URL}/login?cli=true`;
2091
3055
  await (0, import_open.default)(loginUrl);
2092
3056
  const { token } = await import_inquirer.default.prompt([
@@ -2102,7 +3066,7 @@ Email: ${result.user?.email}`));
2102
3066
  const { data, error } = await supabase.auth.getUser(token);
2103
3067
  if (error || !data.user) {
2104
3068
  spinner.fail("Invalid token");
2105
- console.log(import_chalk4.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."));
2106
3070
  return;
2107
3071
  }
2108
3072
  const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
@@ -2116,7 +3080,7 @@ Email: ${result.user?.email}`));
2116
3080
  }
2117
3081
  saveSession(sessionData.session);
2118
3082
  await fetchAndSaveSubscription();
2119
- spinner.succeed(`Logged in as ${import_chalk4.default.green(data.user.email)}`);
3083
+ spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user.email)}`);
2120
3084
  } else {
2121
3085
  let email = options.email;
2122
3086
  if (!email) {
@@ -2149,11 +3113,11 @@ Email: ${result.user?.email}`));
2149
3113
  });
2150
3114
  if (error) {
2151
3115
  spinner.fail("Login failed");
2152
- console.log(import_chalk4.default.red(`
3116
+ console.log(import_chalk6.default.red(`
2153
3117
  ${error.message}`));
2154
3118
  if (error.message.includes("Invalid login")) {
2155
- console.log(import_chalk4.default.dim("\nDon't have an account? Sign up at:"));
2156
- console.log(import_chalk4.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`));
2157
3121
  }
2158
3122
  return;
2159
3123
  }
@@ -2163,41 +3127,41 @@ ${error.message}`));
2163
3127
  }
2164
3128
  saveSession(data.session);
2165
3129
  await fetchAndSaveSubscription();
2166
- spinner.succeed(`Logged in as ${import_chalk4.default.green(data.user?.email)}`);
3130
+ spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user?.email)}`);
2167
3131
  const { planName, sitesLimit } = getUserInfo();
2168
- console.log(import_chalk4.default.dim(`
3132
+ console.log(import_chalk6.default.dim(`
2169
3133
  Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
2170
3134
  }
2171
3135
  } catch (err) {
2172
3136
  spinner.fail("Login failed");
2173
- console.error(import_chalk4.default.red(err instanceof Error ? err.message : "Unknown error"));
3137
+ console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
2174
3138
  }
2175
3139
  }
2176
3140
  async function logout() {
2177
3141
  if (!isLoggedIn()) {
2178
- console.log(import_chalk4.default.yellow("Not logged in"));
3142
+ console.log(import_chalk6.default.yellow("Not logged in"));
2179
3143
  return;
2180
3144
  }
2181
3145
  const { email } = getUserInfo();
2182
- const spinner = (0, import_ora3.default)("Signing out...").start();
3146
+ const spinner = (0, import_ora5.default)("Signing out...").start();
2183
3147
  try {
2184
3148
  const supabase = getSupabaseClient();
2185
3149
  await supabase.auth.signOut();
2186
3150
  clearCredentials();
2187
- spinner.succeed(`Logged out from ${import_chalk4.default.dim(email)}`);
3151
+ spinner.succeed(`Logged out from ${import_chalk6.default.dim(email)}`);
2188
3152
  } catch (err) {
2189
3153
  spinner.fail("Logout failed");
2190
- console.error(import_chalk4.default.red(err instanceof Error ? err.message : "Unknown error"));
3154
+ console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
2191
3155
  }
2192
3156
  }
2193
3157
  async function whoami() {
2194
3158
  if (!isLoggedIn()) {
2195
- console.log(import_chalk4.default.yellow("Not logged in"));
2196
- console.log(import_chalk4.default.dim('\nRun "rankcli login" to sign in.'));
2197
- console.log(import_chalk4.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`));
2198
3162
  return;
2199
3163
  }
2200
- const spinner = (0, import_ora3.default)("Checking account...").start();
3164
+ const spinner = (0, import_ora5.default)("Checking account...").start();
2201
3165
  try {
2202
3166
  if (isApiKeyAuth()) {
2203
3167
  const apiKey = getApiKey();
@@ -2217,18 +3181,18 @@ async function whoami() {
2217
3181
  const { email, planName, sitesLimit } = getUserInfo();
2218
3182
  const authMethod = isApiKeyAuth() ? "API Key" : "Session";
2219
3183
  spinner.stop();
2220
- console.log(import_chalk4.default.bold("\nRankCLI Account"));
3184
+ console.log(import_chalk6.default.bold("\nRankCLI Account"));
2221
3185
  console.log("\u2500".repeat(40));
2222
- console.log(` Email: ${import_chalk4.default.cyan(email || "N/A")}`);
2223
- console.log(` Plan: ${import_chalk4.default.green(planName)}`);
3186
+ console.log(` Email: ${import_chalk6.default.cyan(email || "N/A")}`);
3187
+ console.log(` Plan: ${import_chalk6.default.green(planName)}`);
2224
3188
  console.log(` Sites: ${sitesLimit} allowed`);
2225
- console.log(` Auth: ${import_chalk4.default.dim(authMethod)}`);
3189
+ console.log(` Auth: ${import_chalk6.default.dim(authMethod)}`);
2226
3190
  console.log("\u2500".repeat(40));
2227
- console.log(import_chalk4.default.dim(`
3191
+ console.log(import_chalk6.default.dim(`
2228
3192
  Manage at: ${WEB_APP_URL}/account`));
2229
3193
  } catch (err) {
2230
3194
  spinner.fail("Failed to fetch account info");
2231
- console.error(import_chalk4.default.red(err instanceof Error ? err.message : "Unknown error"));
3195
+ console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
2232
3196
  }
2233
3197
  }
2234
3198
  async function fetchAndSaveSubscription() {
@@ -2260,7 +3224,7 @@ async function fetchAndSaveSubscription() {
2260
3224
  }
2261
3225
 
2262
3226
  // src/version.ts
2263
- var VERSION = "0.0.19";
3227
+ var VERSION = "0.0.21";
2264
3228
 
2265
3229
  // src/index.ts
2266
3230
  var program = new import_commander.Command();
@@ -2272,6 +3236,20 @@ program.command("init").description("Initialize SEO Autopilot in your project").
2272
3236
  program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
2273
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);
2274
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
+ });
2275
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);
2276
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) => {
2277
3255
  if (options.ai && options.url) {
@@ -2294,8 +3272,42 @@ program.command("intent").description("Classify search intent for a keyword").ar
2294
3272
  program.command("competitors").description("Analyze competitor SEO strategies").option("-d, --domain <domain>", "Competitor domain to analyze").action(() => {
2295
3273
  console.log("Competitor analysis coming soon...");
2296
3274
  });
2297
- program.command("rank").description("Check current keyword rankings").action(() => {
2298
- 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
+ });
2299
3311
  });
2300
3312
  program.command("report").description("Generate SEO performance report").option("-f, --format <format>", "Report format (pdf, html, json)", "html").action(() => {
2301
3313
  console.log("Report generation coming soon...");
@@ -2305,4 +3317,5 @@ program.command("watch").description("Continuous monitoring mode").action(() =>
2305
3317
  });
2306
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);
2307
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);
2308
3321
  program.parse();