@rankcli/agent-runtime 0.0.17 → 0.0.18

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.d.mts CHANGED
@@ -644,6 +644,7 @@ interface AIBotBlockingResult {
644
644
  blockedBots: string[];
645
645
  allowedBots: string[];
646
646
  allBlocked: boolean;
647
+ contentSignal: string | null;
647
648
  }
648
649
  interface CloudflareAICrawlerGateResult {
649
650
  behindCloudflare: boolean;
@@ -2751,12 +2752,11 @@ interface WorkflowConfig {
2751
2752
  siteUrl: string;
2752
2753
  features: {
2753
2754
  audit: boolean;
2754
- tracking: boolean;
2755
2755
  autoFix: boolean;
2756
2756
  createIssues: boolean;
2757
2757
  createPRs: boolean;
2758
2758
  };
2759
- apiKey?: string;
2759
+ syncToDashboard?: boolean;
2760
2760
  }
2761
2761
  /**
2762
2762
  * Generate GitHub Action workflow YAML
package/dist/index.d.ts CHANGED
@@ -644,6 +644,7 @@ interface AIBotBlockingResult {
644
644
  blockedBots: string[];
645
645
  allowedBots: string[];
646
646
  allBlocked: boolean;
647
+ contentSignal: string | null;
647
648
  }
648
649
  interface CloudflareAICrawlerGateResult {
649
650
  behindCloudflare: boolean;
@@ -2751,12 +2752,11 @@ interface WorkflowConfig {
2751
2752
  siteUrl: string;
2752
2753
  features: {
2753
2754
  audit: boolean;
2754
- tracking: boolean;
2755
2755
  autoFix: boolean;
2756
2756
  createIssues: boolean;
2757
2757
  createPRs: boolean;
2758
2758
  };
2759
- apiKey?: string;
2759
+ syncToDashboard?: boolean;
2760
2760
  }
2761
2761
  /**
2762
2762
  * Generate GitHub Action workflow YAML
package/dist/index.js CHANGED
@@ -6739,7 +6739,7 @@ var ISSUE_DEFINITIONS = {
6739
6739
  title: "No explicit AI crawler rules on a Cloudflare-fronted site",
6740
6740
  description: 'This site appears to be served through Cloudflare but robots.txt has no explicit Allow/Disallow rules for AI crawlers. Cloudflare blocks "mixed-use" AI crawlers by default on ad-hosting zones starting September 15, 2026, and is rolling out Pay Per Crawl / Pay Per Use gating beyond that.',
6741
6741
  impact: "Without an explicit rule, whether AI crawlers (and future citation opportunities in ChatGPT, Claude, Perplexity, etc.) can reach this site now depends on Cloudflare account-level bot-management defaults, not on this codebase \u2014 a silent, invisible-to-git failure mode.",
6742
- howToFix: "Add explicit User-agent rules for GPTBot, Claude-Web/ClaudeBot, PerplexityBot, and Google-Extended in robots.txt, and confirm the matching allow/block posture in the Cloudflare dashboard under Bot Management \u2192 AI Crawl Control. If you'd rather charge than block, Cloudflare's Pay Per Crawl (AI Crawl Control \u2192 Payments tab) lets you set a per-crawl price instead of a flat allow/deny \u2014 worth a look if you get meaningful AI-crawler traffic."
6742
+ howToFix: `Two ways to make your stance explicit, either works: (1) add a Content-Signal line to robots.txt - e.g. "Content-Signal: search=yes,ai-train=no,use=reference" - the format Cloudflare's AI Crawl Control update actually reads, or (2) add explicit User-agent rules for GPTBot, Claude-Web/ClaudeBot, PerplexityBot, and Google-Extended. Either way, confirm the matching allow/block posture in the Cloudflare dashboard under Bot Management \u2192 AI Crawl Control. If you'd rather charge than block, Cloudflare's Pay Per Crawl (AI Crawl Control \u2192 Payments tab) lets you set a per-crawl price instead of a flat allow/deny \u2014 worth a look if you get meaningful AI-crawler traffic.`
6743
6743
  },
6744
6744
  NO_AGENT_EXPERIENCE_SURFACE: {
6745
6745
  code: "NO_AGENT_EXPERIENCE_SURFACE",
@@ -10467,6 +10467,15 @@ async function checkLlmsTxt(baseUrl) {
10467
10467
  };
10468
10468
  }
10469
10469
  }
10470
+ function parseContentSignal(robotsTxtContent) {
10471
+ for (const line of robotsTxtContent.split("\n")) {
10472
+ const trimmed = line.trim();
10473
+ if (trimmed.toLowerCase().startsWith("content-signal:")) {
10474
+ return trimmed.substring(trimmed.indexOf(":") + 1).trim();
10475
+ }
10476
+ }
10477
+ return null;
10478
+ }
10470
10479
  async function checkAIBotBlocking(baseUrl) {
10471
10480
  const issues = [];
10472
10481
  const url = new URL("/robots.txt", baseUrl).href;
@@ -10484,17 +10493,21 @@ async function checkAIBotBlocking(baseUrl) {
10484
10493
  robotsExists: false,
10485
10494
  blockedBots: [],
10486
10495
  allowedBots: Object.keys(AI_BOTS),
10487
- allBlocked: false
10496
+ allBlocked: false,
10497
+ contentSignal: null
10488
10498
  }
10489
10499
  };
10490
10500
  }
10491
10501
  const content = response.data;
10492
10502
  const lines = content.split("\n");
10503
+ const contentSignal = parseContentSignal(content);
10493
10504
  let currentUserAgent = "";
10494
10505
  const botRules = {};
10495
10506
  for (const line of lines) {
10496
10507
  const trimmed = line.trim().toLowerCase();
10497
- if (trimmed.startsWith("user-agent:")) {
10508
+ if (trimmed.startsWith("content-signal:")) {
10509
+ continue;
10510
+ } else if (trimmed.startsWith("user-agent:")) {
10498
10511
  currentUserAgent = trimmed.split(":")[1].trim();
10499
10512
  } else if (trimmed.startsWith("disallow:")) {
10500
10513
  const path3 = trimmed.split(":")[1]?.trim() || "";
@@ -10567,7 +10580,8 @@ async function checkAIBotBlocking(baseUrl) {
10567
10580
  robotsExists: true,
10568
10581
  blockedBots,
10569
10582
  allowedBots,
10570
- allBlocked: blockedBots.length === Object.keys(AI_BOTS).length
10583
+ allBlocked: blockedBots.length === Object.keys(AI_BOTS).length,
10584
+ contentSignal
10571
10585
  }
10572
10586
  };
10573
10587
  } catch (error) {
@@ -10577,7 +10591,8 @@ async function checkAIBotBlocking(baseUrl) {
10577
10591
  robotsExists: false,
10578
10592
  blockedBots: [],
10579
10593
  allowedBots: Object.keys(AI_BOTS),
10580
- allBlocked: false
10594
+ allBlocked: false,
10595
+ contentSignal: null
10581
10596
  }
10582
10597
  };
10583
10598
  }
@@ -10631,7 +10646,7 @@ async function checkCloudflareAICrawlerGate(baseUrl, botBlocking) {
10631
10646
  } catch {
10632
10647
  behindCloudflare = false;
10633
10648
  }
10634
- const hasExplicitAIRules = botBlocking.robotsExists && botBlocking.blockedBots.length > 0;
10649
+ const hasExplicitAIRules = botBlocking.robotsExists && (botBlocking.blockedBots.length > 0 || !!botBlocking.contentSignal);
10635
10650
  const ambiguous = behindCloudflare && !hasExplicitAIRules;
10636
10651
  if (ambiguous) {
10637
10652
  issues.push({
@@ -25149,32 +25164,16 @@ This will open a browser for OAuth consent and save tokens locally.
25149
25164
  // src/tracking/github-action.ts
25150
25165
  function generateWorkflow(config) {
25151
25166
  const cronSchedule = getCronSchedule(config.schedule);
25152
- return `# SEO Autopilot - Automated SEO Monitoring
25153
- # This workflow runs scheduled SEO audits and tracking
25167
+ return `# RankCLI - Automated SEO Monitoring
25168
+ # https://rankcli.dev - generated by \`rankcli setup --github-action\`
25154
25169
 
25155
- name: SEO Monitoring
25170
+ name: RankCLI SEO Check
25156
25171
 
25157
25172
  on:
25158
- # Scheduled runs
25159
25173
  schedule:
25160
25174
  - cron: '${cronSchedule}'
25161
25175
 
25162
- # Manual trigger
25163
25176
  workflow_dispatch:
25164
- inputs:
25165
- mode:
25166
- description: 'Run mode'
25167
- required: true
25168
- default: 'full'
25169
- type: choice
25170
- options:
25171
- - full
25172
- - audit-only
25173
- - track-only
25174
-
25175
- # Run on main branch pushes (optional)
25176
- # push:
25177
- # branches: [main]
25178
25177
 
25179
25178
  env:
25180
25179
  SITE_URL: '${config.siteUrl}'
@@ -25196,94 +25195,81 @@ jobs:
25196
25195
  with:
25197
25196
  node-version: '20'
25198
25197
 
25199
- - name: Install SEO Autopilot
25200
- run: npm install -g @seo-autopilot/cli
25201
-
25202
- ${config.features.tracking ? `
25203
- - name: Pull GSC Data
25204
- if: \${{ inputs.mode != 'audit-only' }}
25198
+ - name: Install RankCLI
25199
+ run: npm install -g @rankcli/cli
25200
+ ${config.syncToDashboard ? `
25201
+ - name: Log in to RankCLI
25205
25202
  env:
25206
- GSC_SERVICE_ACCOUNT_EMAIL: \${{ secrets.GSC_SERVICE_ACCOUNT_EMAIL }}
25207
- GSC_PRIVATE_KEY: \${{ secrets.GSC_PRIVATE_KEY }}
25203
+ RANKCLI_API_KEY: \${{ secrets.RANKCLI_API_KEY }}
25204
+ run: rankcli login --token "$RANKCLI_API_KEY"
25205
+ ` : ""}
25206
+ - name: Run SEO audit
25208
25207
  run: |
25209
- seo track --site "$SITE_URL" --output tracking-report.json
25210
- continue-on-error: true
25211
- ` : ""}
25212
-
25213
- ${config.features.audit ? `
25214
- - name: Run SEO Audit
25215
- if: \${{ inputs.mode != 'track-only' }}
25216
- run: |
25217
- seo audit --url "$SITE_URL" --output json > audit-report.json
25218
- ` : ""}
25219
-
25220
- - name: Generate Report
25221
- id: report
25222
- run: |
25223
- seo report --combine \\
25224
- ${config.features.tracking ? "--tracking tracking-report.json \\" : ""}
25225
- ${config.features.audit ? "--audit audit-report.json \\" : ""}
25226
- --format markdown > seo-report.md
25227
-
25228
- # Set output for issue creation
25229
- echo "report<<EOF" >> $GITHUB_OUTPUT
25230
- cat seo-report.md >> $GITHUB_OUTPUT
25231
- echo "EOF" >> $GITHUB_OUTPUT
25232
-
25233
- ${config.features.createIssues ? `
25234
- - name: Create/Update Issue
25208
+ rankcli audit --url "$SITE_URL" --output json > audit-report.json
25209
+ ${config.features.createIssues ? `
25210
+ - name: Create/update SEO report issue
25235
25211
  uses: actions/github-script@v7
25236
25212
  with:
25237
25213
  script: |
25214
+ const fs = require('fs');
25215
+ const report = JSON.parse(fs.readFileSync('audit-report.json', 'utf8'));
25216
+ const errors = report.issues.filter(i => i.severity === 'error');
25217
+ const warnings = report.issues.filter(i => i.severity === 'warning');
25218
+ const notices = report.issues.filter(i => i.severity === 'notice');
25219
+
25220
+ const lines = [
25221
+ \`## RankCLI SEO Report\`,
25222
+ '',
25223
+ \`**Score:** \${report.score}/100 \xB7 **Errors:** \${errors.length} \xB7 **Warnings:** \${warnings.length} \xB7 **Notices:** \${notices.length}\`,
25224
+ '',
25225
+ ];
25226
+ if (errors.length > 0) {
25227
+ lines.push('### Errors', ...errors.slice(0, 15).map(i => \`- \${i.title}\`), '');
25228
+ }
25229
+ if (warnings.length > 0) {
25230
+ lines.push('### Warnings', ...warnings.slice(0, 15).map(i => \`- \${i.title}\`), '');
25231
+ }
25232
+ if (report.dashboardUrl) {
25233
+ lines.push(\`[View full report on the RankCLI dashboard](\${report.dashboardUrl})\`);
25234
+ }
25235
+ const body = lines.join('\\n');
25238
25236
  const title = 'SEO Report - ' + new Date().toISOString().split('T')[0];
25239
- const body = \`\${{ steps.report.outputs.report }}\`;
25240
25237
 
25241
- // Find existing issue
25242
25238
  const issues = await github.rest.issues.listForRepo({
25243
25239
  owner: context.repo.owner,
25244
25240
  repo: context.repo.repo,
25245
25241
  labels: 'seo-report',
25246
- state: 'open'
25242
+ state: 'open',
25247
25243
  });
25248
25244
 
25249
25245
  if (issues.data.length > 0) {
25250
- // Update existing issue
25251
25246
  await github.rest.issues.update({
25252
25247
  owner: context.repo.owner,
25253
25248
  repo: context.repo.repo,
25254
25249
  issue_number: issues.data[0].number,
25255
- body: body
25250
+ body,
25256
25251
  });
25257
- console.log('Updated issue #' + issues.data[0].number);
25258
25252
  } else {
25259
- // Create new issue
25260
25253
  await github.rest.issues.create({
25261
25254
  owner: context.repo.owner,
25262
25255
  repo: context.repo.repo,
25263
- title: title,
25264
- body: body,
25265
- labels: ['seo-report', 'automated']
25256
+ title,
25257
+ body,
25258
+ labels: ['seo-report', 'automated'],
25266
25259
  });
25267
- console.log('Created new SEO report issue');
25268
25260
  }
25269
- ` : ""}
25261
+ ` : ""}${config.features.autoFix && config.features.createPRs ? `
25262
+ - name: Auto-fix issues
25263
+ run: rankcli fix --url "$SITE_URL" --auto --output json > fixes.json
25270
25264
 
25271
- ${config.features.autoFix && config.features.createPRs ? `
25272
- - name: Auto-fix Issues
25273
- id: autofix
25265
+ - name: Check if any fixes were applied
25266
+ id: fixes
25274
25267
  run: |
25275
- # Run auto-fixer for safe, non-breaking changes
25276
- seo fix --auto --safe-only --output fixes.json
25277
-
25278
- # Check if any fixes were made
25279
- if [ -s fixes.json ]; then
25280
- echo "fixes_made=true" >> $GITHUB_OUTPUT
25281
- else
25282
- echo "fixes_made=false" >> $GITHUB_OUTPUT
25283
- fi
25284
-
25285
- - name: Create PR with Fixes
25286
- if: steps.autofix.outputs.fixes_made == 'true'
25268
+ APPLIED=$(node -p "JSON.parse(require('fs').readFileSync('fixes.json','utf8')).appliedCount || 0")
25269
+ echo "applied=$APPLIED" >> $GITHUB_OUTPUT
25270
+
25271
+ - name: Create PR with fixes
25272
+ if: steps.fixes.outputs.applied != '0'
25287
25273
  uses: peter-evans/create-pull-request@v6
25288
25274
  with:
25289
25275
  token: \${{ secrets.GITHUB_TOKEN }}
@@ -25292,43 +25278,23 @@ jobs:
25292
25278
  body: |
25293
25279
  ## Automated SEO Fixes
25294
25280
 
25295
- This PR was automatically generated by SEO Autopilot.
25296
-
25297
- ### Changes Made
25298
- \`\`\`
25299
- $(cat fixes.json | jq -r '.fixes[] | "- " + .description')
25300
- \`\`\`
25281
+ This PR was automatically generated by [RankCLI](https://rankcli.dev).
25301
25282
 
25302
25283
  ### Review Checklist
25303
25284
  - [ ] Changes look correct
25304
25285
  - [ ] No unintended modifications
25305
25286
  - [ ] Tests pass
25306
-
25307
- ---
25308
- \u{1F916} Generated by [SEO Autopilot](https://github.com/seo-autopilot)
25309
- branch: seo-autofix-\${{ github.run_number }}
25287
+ branch: rankcli-autofix-\${{ github.run_number }}
25310
25288
  labels: seo, automated
25311
- ` : ""}
25312
-
25313
- - name: Save Report Artifact
25289
+ ` : ""}
25290
+ - name: Save report artifact
25314
25291
  uses: actions/upload-artifact@v4
25315
25292
  with:
25316
- name: seo-report-\${{ github.run_number }}
25293
+ name: rankcli-report-\${{ github.run_number }}
25317
25294
  path: |
25318
- seo-report.md
25319
- ${config.features.audit ? "audit-report.json" : ""}
25320
- ${config.features.tracking ? "tracking-report.json" : ""}
25295
+ audit-report.json
25296
+ ${config.features.autoFix ? "fixes.json" : ""}
25321
25297
  retention-days: 90
25322
-
25323
- ${config.apiKey ? `
25324
- - name: Send to SEO Autopilot Dashboard
25325
- env:
25326
- SEO_AUTOPILOT_API_KEY: \${{ secrets.SEO_AUTOPILOT_API_KEY }}
25327
- run: |
25328
- seo sync --api-key "$SEO_AUTOPILOT_API_KEY" \\
25329
- ${config.features.audit ? "--audit audit-report.json \\" : ""}
25330
- ${config.features.tracking ? "--tracking tracking-report.json" : ""}
25331
- ` : ""}
25332
25298
  `;
25333
25299
  }
25334
25300
  function getCronSchedule(schedule) {
@@ -25355,34 +25321,24 @@ function generateSecretsDoc(config) {
25355
25321
  Go to your repository Settings > Secrets and variables > Actions
25356
25322
 
25357
25323
  `;
25358
- if (config.features.tracking) {
25324
+ if (config.syncToDashboard) {
25359
25325
  secrets += `
25360
- ## Google Search Console (Required for tracking)
25361
-
25362
- 1. \`GSC_SERVICE_ACCOUNT_EMAIL\`
25363
- - Your Google Cloud service account email
25364
- - Example: seo-bot@my-project.iam.gserviceaccount.com
25326
+ ## RankCLI (required - sends audits to your dashboard)
25365
25327
 
25366
- 2. \`GSC_PRIVATE_KEY\`
25367
- - The private key from your service account JSON
25368
- - Include the full key with -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----
25369
-
25370
- ### Setup Instructions:
25371
- 1. Go to https://console.cloud.google.com
25372
- 2. Create a project and enable "Search Console API"
25373
- 3. Create a service account and download JSON key
25374
- 4. In Search Console, add the service account email as a user
25375
- 5. Copy email and private_key to GitHub Secrets
25328
+ 1. \`RANKCLI_API_KEY\`
25329
+ - Get a free API key at https://rankcli.dev/account?tab=api-keys
25330
+ - Without this secret, remove the "Log in to RankCLI" step from
25331
+ .github/workflows/seo.yml - the audit still runs, it just won't
25332
+ appear on your dashboard.
25376
25333
 
25377
25334
  `;
25378
- }
25379
- if (config.apiKey) {
25335
+ } else {
25380
25336
  secrets += `
25381
- ## SEO Autopilot (Required for dashboard sync)
25382
-
25383
- 1. \`SEO_AUTOPILOT_API_KEY\`
25384
- - Get your API key from https://seo-autopilot.dev/dashboard
25385
- - This enables cloud dashboard, historical tracking, and alerts
25337
+ No secrets are required for this workflow as configured - it runs a local
25338
+ audit and, if enabled, opens auto-fix PRs using the repository's built-in
25339
+ GITHUB_TOKEN. To also sync results to your RankCLI dashboard, re-run
25340
+ \`rankcli setup --github-action\` and opt in when asked, or add a
25341
+ \`RANKCLI_API_KEY\` secret and a \`rankcli login --token\` step yourself.
25386
25342
 
25387
25343
  `;
25388
25344
  }
@@ -25410,18 +25366,24 @@ function writeGitHubActionFiles(projectPath, config) {
25410
25366
  const secretsDocPath = path3.join(docsDir, "SEO_SETUP.md");
25411
25367
  fs3.writeFileSync(secretsDocPath, generateSecretsDoc(config));
25412
25368
  files.push(secretsDocPath);
25413
- const instructions = `
25369
+ const instructions = config.syncToDashboard ? `
25414
25370
  GitHub Action created! Next steps:
25415
25371
 
25416
- 1. Add required secrets to your repository:
25372
+ 1. Add the RANKCLI_API_KEY secret to your repository:
25417
25373
  Settings > Secrets and variables > Actions
25374
+ Get a key at https://rankcli.dev/account?tab=api-keys
25418
25375
  See .github/SEO_SETUP.md for details
25419
25376
 
25420
25377
  2. The workflow will run ${config.schedule}
25421
- Or trigger manually: Actions > SEO Monitoring > Run workflow
25378
+ Or trigger manually: Actions > RankCLI SEO Check > Run workflow
25379
+ ` : `
25380
+ GitHub Action created! No secrets required to run as-is.
25422
25381
 
25423
- 3. Add status badge to your README:
25424
- ${generateGitHubActionSetup(config).readmeBadge.replace("YOUR_ORG/YOUR_REPO", "<your-org>/<your-repo>")}
25382
+ 1. Commit and push .github/workflows/seo.yml
25383
+ 2. The workflow will run ${config.schedule}
25384
+ Or trigger manually: Actions > RankCLI SEO Check > Run workflow
25385
+ 3. Want audits on your RankCLI dashboard too? Re-run
25386
+ \`rankcli setup --github-action\` and opt in - see .github/SEO_SETUP.md
25425
25387
  `;
25426
25388
  return { files, instructions };
25427
25389
  }
package/dist/index.mjs CHANGED
@@ -3224,7 +3224,7 @@ var ISSUE_DEFINITIONS = {
3224
3224
  title: "No explicit AI crawler rules on a Cloudflare-fronted site",
3225
3225
  description: 'This site appears to be served through Cloudflare but robots.txt has no explicit Allow/Disallow rules for AI crawlers. Cloudflare blocks "mixed-use" AI crawlers by default on ad-hosting zones starting September 15, 2026, and is rolling out Pay Per Crawl / Pay Per Use gating beyond that.',
3226
3226
  impact: "Without an explicit rule, whether AI crawlers (and future citation opportunities in ChatGPT, Claude, Perplexity, etc.) can reach this site now depends on Cloudflare account-level bot-management defaults, not on this codebase \u2014 a silent, invisible-to-git failure mode.",
3227
- howToFix: "Add explicit User-agent rules for GPTBot, Claude-Web/ClaudeBot, PerplexityBot, and Google-Extended in robots.txt, and confirm the matching allow/block posture in the Cloudflare dashboard under Bot Management \u2192 AI Crawl Control. If you'd rather charge than block, Cloudflare's Pay Per Crawl (AI Crawl Control \u2192 Payments tab) lets you set a per-crawl price instead of a flat allow/deny \u2014 worth a look if you get meaningful AI-crawler traffic."
3227
+ howToFix: `Two ways to make your stance explicit, either works: (1) add a Content-Signal line to robots.txt - e.g. "Content-Signal: search=yes,ai-train=no,use=reference" - the format Cloudflare's AI Crawl Control update actually reads, or (2) add explicit User-agent rules for GPTBot, Claude-Web/ClaudeBot, PerplexityBot, and Google-Extended. Either way, confirm the matching allow/block posture in the Cloudflare dashboard under Bot Management \u2192 AI Crawl Control. If you'd rather charge than block, Cloudflare's Pay Per Crawl (AI Crawl Control \u2192 Payments tab) lets you set a per-crawl price instead of a flat allow/deny \u2014 worth a look if you get meaningful AI-crawler traffic.`
3228
3228
  },
3229
3229
  NO_AGENT_EXPERIENCE_SURFACE: {
3230
3230
  code: "NO_AGENT_EXPERIENCE_SURFACE",
@@ -6940,6 +6940,15 @@ async function checkLlmsTxt(baseUrl) {
6940
6940
  };
6941
6941
  }
6942
6942
  }
6943
+ function parseContentSignal(robotsTxtContent) {
6944
+ for (const line of robotsTxtContent.split("\n")) {
6945
+ const trimmed = line.trim();
6946
+ if (trimmed.toLowerCase().startsWith("content-signal:")) {
6947
+ return trimmed.substring(trimmed.indexOf(":") + 1).trim();
6948
+ }
6949
+ }
6950
+ return null;
6951
+ }
6943
6952
  async function checkAIBotBlocking(baseUrl) {
6944
6953
  const issues = [];
6945
6954
  const url = new URL("/robots.txt", baseUrl).href;
@@ -6957,17 +6966,21 @@ async function checkAIBotBlocking(baseUrl) {
6957
6966
  robotsExists: false,
6958
6967
  blockedBots: [],
6959
6968
  allowedBots: Object.keys(AI_BOTS),
6960
- allBlocked: false
6969
+ allBlocked: false,
6970
+ contentSignal: null
6961
6971
  }
6962
6972
  };
6963
6973
  }
6964
6974
  const content = response.data;
6965
6975
  const lines = content.split("\n");
6976
+ const contentSignal = parseContentSignal(content);
6966
6977
  let currentUserAgent = "";
6967
6978
  const botRules = {};
6968
6979
  for (const line of lines) {
6969
6980
  const trimmed = line.trim().toLowerCase();
6970
- if (trimmed.startsWith("user-agent:")) {
6981
+ if (trimmed.startsWith("content-signal:")) {
6982
+ continue;
6983
+ } else if (trimmed.startsWith("user-agent:")) {
6971
6984
  currentUserAgent = trimmed.split(":")[1].trim();
6972
6985
  } else if (trimmed.startsWith("disallow:")) {
6973
6986
  const path3 = trimmed.split(":")[1]?.trim() || "";
@@ -7040,7 +7053,8 @@ async function checkAIBotBlocking(baseUrl) {
7040
7053
  robotsExists: true,
7041
7054
  blockedBots,
7042
7055
  allowedBots,
7043
- allBlocked: blockedBots.length === Object.keys(AI_BOTS).length
7056
+ allBlocked: blockedBots.length === Object.keys(AI_BOTS).length,
7057
+ contentSignal
7044
7058
  }
7045
7059
  };
7046
7060
  } catch (error) {
@@ -7050,7 +7064,8 @@ async function checkAIBotBlocking(baseUrl) {
7050
7064
  robotsExists: false,
7051
7065
  blockedBots: [],
7052
7066
  allowedBots: Object.keys(AI_BOTS),
7053
- allBlocked: false
7067
+ allBlocked: false,
7068
+ contentSignal: null
7054
7069
  }
7055
7070
  };
7056
7071
  }
@@ -7104,7 +7119,7 @@ async function checkCloudflareAICrawlerGate(baseUrl, botBlocking) {
7104
7119
  } catch {
7105
7120
  behindCloudflare = false;
7106
7121
  }
7107
- const hasExplicitAIRules = botBlocking.robotsExists && botBlocking.blockedBots.length > 0;
7122
+ const hasExplicitAIRules = botBlocking.robotsExists && (botBlocking.blockedBots.length > 0 || !!botBlocking.contentSignal);
7108
7123
  const ambiguous = behindCloudflare && !hasExplicitAIRules;
7109
7124
  if (ambiguous) {
7110
7125
  issues.push({
@@ -21600,32 +21615,16 @@ This will open a browser for OAuth consent and save tokens locally.
21600
21615
  // src/tracking/github-action.ts
21601
21616
  function generateWorkflow(config) {
21602
21617
  const cronSchedule = getCronSchedule(config.schedule);
21603
- return `# SEO Autopilot - Automated SEO Monitoring
21604
- # This workflow runs scheduled SEO audits and tracking
21618
+ return `# RankCLI - Automated SEO Monitoring
21619
+ # https://rankcli.dev - generated by \`rankcli setup --github-action\`
21605
21620
 
21606
- name: SEO Monitoring
21621
+ name: RankCLI SEO Check
21607
21622
 
21608
21623
  on:
21609
- # Scheduled runs
21610
21624
  schedule:
21611
21625
  - cron: '${cronSchedule}'
21612
21626
 
21613
- # Manual trigger
21614
21627
  workflow_dispatch:
21615
- inputs:
21616
- mode:
21617
- description: 'Run mode'
21618
- required: true
21619
- default: 'full'
21620
- type: choice
21621
- options:
21622
- - full
21623
- - audit-only
21624
- - track-only
21625
-
21626
- # Run on main branch pushes (optional)
21627
- # push:
21628
- # branches: [main]
21629
21628
 
21630
21629
  env:
21631
21630
  SITE_URL: '${config.siteUrl}'
@@ -21647,94 +21646,81 @@ jobs:
21647
21646
  with:
21648
21647
  node-version: '20'
21649
21648
 
21650
- - name: Install SEO Autopilot
21651
- run: npm install -g @seo-autopilot/cli
21652
-
21653
- ${config.features.tracking ? `
21654
- - name: Pull GSC Data
21655
- if: \${{ inputs.mode != 'audit-only' }}
21649
+ - name: Install RankCLI
21650
+ run: npm install -g @rankcli/cli
21651
+ ${config.syncToDashboard ? `
21652
+ - name: Log in to RankCLI
21656
21653
  env:
21657
- GSC_SERVICE_ACCOUNT_EMAIL: \${{ secrets.GSC_SERVICE_ACCOUNT_EMAIL }}
21658
- GSC_PRIVATE_KEY: \${{ secrets.GSC_PRIVATE_KEY }}
21654
+ RANKCLI_API_KEY: \${{ secrets.RANKCLI_API_KEY }}
21655
+ run: rankcli login --token "$RANKCLI_API_KEY"
21656
+ ` : ""}
21657
+ - name: Run SEO audit
21659
21658
  run: |
21660
- seo track --site "$SITE_URL" --output tracking-report.json
21661
- continue-on-error: true
21662
- ` : ""}
21663
-
21664
- ${config.features.audit ? `
21665
- - name: Run SEO Audit
21666
- if: \${{ inputs.mode != 'track-only' }}
21667
- run: |
21668
- seo audit --url "$SITE_URL" --output json > audit-report.json
21669
- ` : ""}
21670
-
21671
- - name: Generate Report
21672
- id: report
21673
- run: |
21674
- seo report --combine \\
21675
- ${config.features.tracking ? "--tracking tracking-report.json \\" : ""}
21676
- ${config.features.audit ? "--audit audit-report.json \\" : ""}
21677
- --format markdown > seo-report.md
21678
-
21679
- # Set output for issue creation
21680
- echo "report<<EOF" >> $GITHUB_OUTPUT
21681
- cat seo-report.md >> $GITHUB_OUTPUT
21682
- echo "EOF" >> $GITHUB_OUTPUT
21683
-
21684
- ${config.features.createIssues ? `
21685
- - name: Create/Update Issue
21659
+ rankcli audit --url "$SITE_URL" --output json > audit-report.json
21660
+ ${config.features.createIssues ? `
21661
+ - name: Create/update SEO report issue
21686
21662
  uses: actions/github-script@v7
21687
21663
  with:
21688
21664
  script: |
21665
+ const fs = require('fs');
21666
+ const report = JSON.parse(fs.readFileSync('audit-report.json', 'utf8'));
21667
+ const errors = report.issues.filter(i => i.severity === 'error');
21668
+ const warnings = report.issues.filter(i => i.severity === 'warning');
21669
+ const notices = report.issues.filter(i => i.severity === 'notice');
21670
+
21671
+ const lines = [
21672
+ \`## RankCLI SEO Report\`,
21673
+ '',
21674
+ \`**Score:** \${report.score}/100 \xB7 **Errors:** \${errors.length} \xB7 **Warnings:** \${warnings.length} \xB7 **Notices:** \${notices.length}\`,
21675
+ '',
21676
+ ];
21677
+ if (errors.length > 0) {
21678
+ lines.push('### Errors', ...errors.slice(0, 15).map(i => \`- \${i.title}\`), '');
21679
+ }
21680
+ if (warnings.length > 0) {
21681
+ lines.push('### Warnings', ...warnings.slice(0, 15).map(i => \`- \${i.title}\`), '');
21682
+ }
21683
+ if (report.dashboardUrl) {
21684
+ lines.push(\`[View full report on the RankCLI dashboard](\${report.dashboardUrl})\`);
21685
+ }
21686
+ const body = lines.join('\\n');
21689
21687
  const title = 'SEO Report - ' + new Date().toISOString().split('T')[0];
21690
- const body = \`\${{ steps.report.outputs.report }}\`;
21691
21688
 
21692
- // Find existing issue
21693
21689
  const issues = await github.rest.issues.listForRepo({
21694
21690
  owner: context.repo.owner,
21695
21691
  repo: context.repo.repo,
21696
21692
  labels: 'seo-report',
21697
- state: 'open'
21693
+ state: 'open',
21698
21694
  });
21699
21695
 
21700
21696
  if (issues.data.length > 0) {
21701
- // Update existing issue
21702
21697
  await github.rest.issues.update({
21703
21698
  owner: context.repo.owner,
21704
21699
  repo: context.repo.repo,
21705
21700
  issue_number: issues.data[0].number,
21706
- body: body
21701
+ body,
21707
21702
  });
21708
- console.log('Updated issue #' + issues.data[0].number);
21709
21703
  } else {
21710
- // Create new issue
21711
21704
  await github.rest.issues.create({
21712
21705
  owner: context.repo.owner,
21713
21706
  repo: context.repo.repo,
21714
- title: title,
21715
- body: body,
21716
- labels: ['seo-report', 'automated']
21707
+ title,
21708
+ body,
21709
+ labels: ['seo-report', 'automated'],
21717
21710
  });
21718
- console.log('Created new SEO report issue');
21719
21711
  }
21720
- ` : ""}
21712
+ ` : ""}${config.features.autoFix && config.features.createPRs ? `
21713
+ - name: Auto-fix issues
21714
+ run: rankcli fix --url "$SITE_URL" --auto --output json > fixes.json
21721
21715
 
21722
- ${config.features.autoFix && config.features.createPRs ? `
21723
- - name: Auto-fix Issues
21724
- id: autofix
21716
+ - name: Check if any fixes were applied
21717
+ id: fixes
21725
21718
  run: |
21726
- # Run auto-fixer for safe, non-breaking changes
21727
- seo fix --auto --safe-only --output fixes.json
21728
-
21729
- # Check if any fixes were made
21730
- if [ -s fixes.json ]; then
21731
- echo "fixes_made=true" >> $GITHUB_OUTPUT
21732
- else
21733
- echo "fixes_made=false" >> $GITHUB_OUTPUT
21734
- fi
21735
-
21736
- - name: Create PR with Fixes
21737
- if: steps.autofix.outputs.fixes_made == 'true'
21719
+ APPLIED=$(node -p "JSON.parse(require('fs').readFileSync('fixes.json','utf8')).appliedCount || 0")
21720
+ echo "applied=$APPLIED" >> $GITHUB_OUTPUT
21721
+
21722
+ - name: Create PR with fixes
21723
+ if: steps.fixes.outputs.applied != '0'
21738
21724
  uses: peter-evans/create-pull-request@v6
21739
21725
  with:
21740
21726
  token: \${{ secrets.GITHUB_TOKEN }}
@@ -21743,43 +21729,23 @@ jobs:
21743
21729
  body: |
21744
21730
  ## Automated SEO Fixes
21745
21731
 
21746
- This PR was automatically generated by SEO Autopilot.
21747
-
21748
- ### Changes Made
21749
- \`\`\`
21750
- $(cat fixes.json | jq -r '.fixes[] | "- " + .description')
21751
- \`\`\`
21732
+ This PR was automatically generated by [RankCLI](https://rankcli.dev).
21752
21733
 
21753
21734
  ### Review Checklist
21754
21735
  - [ ] Changes look correct
21755
21736
  - [ ] No unintended modifications
21756
21737
  - [ ] Tests pass
21757
-
21758
- ---
21759
- \u{1F916} Generated by [SEO Autopilot](https://github.com/seo-autopilot)
21760
- branch: seo-autofix-\${{ github.run_number }}
21738
+ branch: rankcli-autofix-\${{ github.run_number }}
21761
21739
  labels: seo, automated
21762
- ` : ""}
21763
-
21764
- - name: Save Report Artifact
21740
+ ` : ""}
21741
+ - name: Save report artifact
21765
21742
  uses: actions/upload-artifact@v4
21766
21743
  with:
21767
- name: seo-report-\${{ github.run_number }}
21744
+ name: rankcli-report-\${{ github.run_number }}
21768
21745
  path: |
21769
- seo-report.md
21770
- ${config.features.audit ? "audit-report.json" : ""}
21771
- ${config.features.tracking ? "tracking-report.json" : ""}
21746
+ audit-report.json
21747
+ ${config.features.autoFix ? "fixes.json" : ""}
21772
21748
  retention-days: 90
21773
-
21774
- ${config.apiKey ? `
21775
- - name: Send to SEO Autopilot Dashboard
21776
- env:
21777
- SEO_AUTOPILOT_API_KEY: \${{ secrets.SEO_AUTOPILOT_API_KEY }}
21778
- run: |
21779
- seo sync --api-key "$SEO_AUTOPILOT_API_KEY" \\
21780
- ${config.features.audit ? "--audit audit-report.json \\" : ""}
21781
- ${config.features.tracking ? "--tracking tracking-report.json" : ""}
21782
- ` : ""}
21783
21749
  `;
21784
21750
  }
21785
21751
  function getCronSchedule(schedule) {
@@ -21806,34 +21772,24 @@ function generateSecretsDoc(config) {
21806
21772
  Go to your repository Settings > Secrets and variables > Actions
21807
21773
 
21808
21774
  `;
21809
- if (config.features.tracking) {
21775
+ if (config.syncToDashboard) {
21810
21776
  secrets += `
21811
- ## Google Search Console (Required for tracking)
21812
-
21813
- 1. \`GSC_SERVICE_ACCOUNT_EMAIL\`
21814
- - Your Google Cloud service account email
21815
- - Example: seo-bot@my-project.iam.gserviceaccount.com
21777
+ ## RankCLI (required - sends audits to your dashboard)
21816
21778
 
21817
- 2. \`GSC_PRIVATE_KEY\`
21818
- - The private key from your service account JSON
21819
- - Include the full key with -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----
21820
-
21821
- ### Setup Instructions:
21822
- 1. Go to https://console.cloud.google.com
21823
- 2. Create a project and enable "Search Console API"
21824
- 3. Create a service account and download JSON key
21825
- 4. In Search Console, add the service account email as a user
21826
- 5. Copy email and private_key to GitHub Secrets
21779
+ 1. \`RANKCLI_API_KEY\`
21780
+ - Get a free API key at https://rankcli.dev/account?tab=api-keys
21781
+ - Without this secret, remove the "Log in to RankCLI" step from
21782
+ .github/workflows/seo.yml - the audit still runs, it just won't
21783
+ appear on your dashboard.
21827
21784
 
21828
21785
  `;
21829
- }
21830
- if (config.apiKey) {
21786
+ } else {
21831
21787
  secrets += `
21832
- ## SEO Autopilot (Required for dashboard sync)
21833
-
21834
- 1. \`SEO_AUTOPILOT_API_KEY\`
21835
- - Get your API key from https://seo-autopilot.dev/dashboard
21836
- - This enables cloud dashboard, historical tracking, and alerts
21788
+ No secrets are required for this workflow as configured - it runs a local
21789
+ audit and, if enabled, opens auto-fix PRs using the repository's built-in
21790
+ GITHUB_TOKEN. To also sync results to your RankCLI dashboard, re-run
21791
+ \`rankcli setup --github-action\` and opt in when asked, or add a
21792
+ \`RANKCLI_API_KEY\` secret and a \`rankcli login --token\` step yourself.
21837
21793
 
21838
21794
  `;
21839
21795
  }
@@ -21861,18 +21817,24 @@ function writeGitHubActionFiles(projectPath, config) {
21861
21817
  const secretsDocPath = path3.join(docsDir, "SEO_SETUP.md");
21862
21818
  fs3.writeFileSync(secretsDocPath, generateSecretsDoc(config));
21863
21819
  files.push(secretsDocPath);
21864
- const instructions = `
21820
+ const instructions = config.syncToDashboard ? `
21865
21821
  GitHub Action created! Next steps:
21866
21822
 
21867
- 1. Add required secrets to your repository:
21823
+ 1. Add the RANKCLI_API_KEY secret to your repository:
21868
21824
  Settings > Secrets and variables > Actions
21825
+ Get a key at https://rankcli.dev/account?tab=api-keys
21869
21826
  See .github/SEO_SETUP.md for details
21870
21827
 
21871
21828
  2. The workflow will run ${config.schedule}
21872
- Or trigger manually: Actions > SEO Monitoring > Run workflow
21829
+ Or trigger manually: Actions > RankCLI SEO Check > Run workflow
21830
+ ` : `
21831
+ GitHub Action created! No secrets required to run as-is.
21873
21832
 
21874
- 3. Add status badge to your README:
21875
- ${generateGitHubActionSetup(config).readmeBadge.replace("YOUR_ORG/YOUR_REPO", "<your-org>/<your-repo>")}
21833
+ 1. Commit and push .github/workflows/seo.yml
21834
+ 2. The workflow will run ${config.schedule}
21835
+ Or trigger manually: Actions > RankCLI SEO Check > Run workflow
21836
+ 3. Want audits on your RankCLI dashboard too? Re-run
21837
+ \`rankcli setup --github-action\` and opt in - see .github/SEO_SETUP.md
21876
21838
  `;
21877
21839
  return { files, instructions };
21878
21840
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankcli/agent-runtime",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "RankCLI agent runtime - executes SEO audits and fixes with AI",
5
5
  "homepage": "https://rankcli.dev",
6
6
  "main": "dist/index.js",