@rankcli/cli 0.0.30 → 0.0.32

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/README.md CHANGED
@@ -157,7 +157,11 @@ The gates are opt-in: without them, a completed audit exits `0` however many iss
157
157
 
158
158
  #### AI analysis (`--ai`)
159
159
 
160
- AI analysis runs on your own provider - nothing goes through RankCLI. With no `--ai-provider`, the first key found wins, in this order (OpenRouter first):
160
+ AI analysis runs on your own provider - nothing goes through RankCLI.
161
+
162
+ **On a Mac with Apple Intelligence, it's the default.** On macOS 26 or later on Apple silicon with Apple Intelligence turned on, `--ai` uses Apple's on-device model: free, private, no key and no network round trip. The first run builds a small Swift helper with the Xcode command-line tools (a few seconds, cached in `~/Library/Caches/rankcli`). The on-device model has a small context window, so it sees the 25 most severe issues. To use another provider, pass `--ai-provider`, or set it once with `RANKCLI_AI_PROVIDER=openrouter` (any provider id below).
163
+
164
+ Otherwise, with no `--ai-provider`, the first key found wins, in this order (OpenRouter first):
161
165
 
162
166
  | Provider | Env var | Base URL | Default model |
163
167
  |----------|---------|----------|---------------|
@@ -256,10 +260,10 @@ jobs:
256
260
  - run: rankcli audit --url "$SITE_URL" --fail-on error --min-score 80 -o json > "$RUNNER_TEMP/audit.json"
257
261
  env:
258
262
  SITE_URL: ${{ vars.SITE_URL }}
259
- RANKCLI_API_KEY: ${{ secrets.RANKCLI_API_KEY }} # optional: paid-plan checks + dashboard sync
263
+ RANKCLI_API_KEY: ${{ secrets.RANKCLI_API_KEY }} # optional: dashboard sync + your plan's limits
260
264
  ```
261
265
 
262
- `rankcli setup --github-action --url https://yoursite.com [--sync]` writes a scheduled workflow that files the report as an issue (and, with `--sync` and a Solo+ key, opens auto-fix PRs).
266
+ `rankcli setup --github-action --url https://yoursite.com [--sync]` writes a scheduled workflow that files the report as an issue (and, with `--sync` and a RankCLI API key, opens auto-fix PRs - 3 a month on Free).
263
267
 
264
268
  ## Environment Variables
265
269
 
@@ -278,11 +282,13 @@ The npm package needs **Node 20.19+ or 22.12+** (any 23+ works). The CLI is a Co
278
282
 
279
283
  | Feature | Free | Solo+ (from $9/mo) |
280
284
  |---------|------|--------------|
281
- | SEO Checks | 100 | 280+ |
282
- | GEO Analysis | Basic | Full |
283
- | Framework Fixes | - | ✅ 25+ |
284
- | Auto-Fix PRs | - | ✅ |
285
- | Sites | 1 | 3+ |
285
+ | SEO + GEO checks | All 280+ | All 280+ |
286
+ | Local fixes (`rankcli fix`, 25+ frameworks) | ✅ | ✅ |
287
+ | Auto-fix PRs (GitHub App) | 3 a month | ✅ |
288
+ | Hosted sites | 1, audited weekly | 3+, weekly or daily |
289
+ | Slack & Discord alerts | - | ✅ |
290
+
291
+ See [rankcli.dev/pricing](https://rankcli.dev/pricing).
286
292
 
287
293
  ## Links
288
294
 
package/dist/index.js CHANGED
@@ -45,19 +45,19 @@ var import_node_crypto = require("crypto");
45
45
  var import_supabase_js = require("@supabase/supabase-js");
46
46
  var INJECTED_CONFIG = {"supabaseUrl":"https://bspljbxwbjiqueeyzyat.supabase.co","supabaseAnonKey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJzcGxqYnh3YmppcXVlZXl6eWF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNzQ1NTksImV4cCI6MjA4NDg1MDU1OX0.7gprxM2gT9cZ99sjZURLltrln78dEnwyFFKYpTzBaUU","webAppUrl":"https://rankcli.dev","decryptPassphrase":"lKFLeIw/xGPa6E8c4XswZX/oiLtJf/yLF/Y6zwovte8="};
47
47
  function getSupabaseUrl() {
48
- const url = process.env.RANKCLI_SUPABASE_URL || process.env.VITE_SUPABASE_URL || INJECTED_CONFIG?.supabaseUrl;
48
+ const url = process.env.RANKCLI_SUPABASE_URL || INJECTED_CONFIG?.supabaseUrl;
49
49
  if (!url) {
50
50
  throw new Error(
51
- "Supabase URL not configured. Set RANKCLI_SUPABASE_URL environment variable.\nSee: https://rankcli.dev/docs/cli-setup"
51
+ "Supabase URL not configured. Set RANKCLI_SUPABASE_URL environment variable.\nSee: https://rankcli.dev/docs#getting-started"
52
52
  );
53
53
  }
54
54
  return url;
55
55
  }
56
56
  function getSupabaseAnonKey() {
57
- const key = process.env.RANKCLI_SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || INJECTED_CONFIG?.supabaseAnonKey;
57
+ const key = process.env.RANKCLI_SUPABASE_ANON_KEY || INJECTED_CONFIG?.supabaseAnonKey;
58
58
  if (!key) {
59
59
  throw new Error(
60
- "Supabase anon key not configured. Set RANKCLI_SUPABASE_ANON_KEY environment variable.\nSee: https://rankcli.dev/docs/cli-setup"
60
+ "Supabase anon key not configured. Set RANKCLI_SUPABASE_ANON_KEY environment variable.\nSee: https://rankcli.dev/docs#getting-started"
61
61
  );
62
62
  }
63
63
  return key;
@@ -1110,23 +1110,25 @@ var import_chalk4 = __toESM(require("chalk"));
1110
1110
  var import_chalk3 = __toESM(require("chalk"));
1111
1111
  var WEB_APP_URL2 = "https://rankcli.dev";
1112
1112
  var TIER_LIMITS = {
1113
+ // Every plan runs every check (pricing-tiers.json: free_audit / free_registered 280). Plans
1114
+ // differ in sites, schedules, pages per audit, PRs and alerts - not in what an audit checks.
1113
1115
  free_anon: {
1114
- checks: 50,
1116
+ checks: 280,
1115
1117
  sites: 0,
1116
1118
  domains_per_month: 1,
1117
- features: { geo: false, auto_fix: false, github: false, ai: false }
1119
+ features: { geo: true, auto_fix: true, github: false, ai: false }
1118
1120
  },
1119
1121
  free: {
1120
- checks: 100,
1122
+ checks: 280,
1121
1123
  sites: 1,
1122
1124
  domains_per_month: 2,
1123
- features: { geo: "basic", auto_fix: false, github: false, ai: false }
1125
+ features: { geo: true, auto_fix: true, github: true, ai: false }
1124
1126
  },
1125
1127
  solo: {
1126
1128
  checks: 280,
1127
1129
  sites: 3,
1128
1130
  domains_per_month: 5,
1129
- features: { geo: true, auto_fix: true, github: false, ai: true }
1131
+ features: { geo: true, auto_fix: true, github: true, ai: true }
1130
1132
  },
1131
1133
  pro: {
1132
1134
  checks: 280,
@@ -1152,21 +1154,14 @@ var UPSELL_MESSAGES = {
1152
1154
  free_audit_complete: () => [
1153
1155
  "",
1154
1156
  import_chalk3.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1155
- import_chalk3.default.yellow.bold(" \u{1F680} Get 2x more checks + audit history"),
1156
- import_chalk3.default.dim(" Create a free account to unlock 100 checks and save your results."),
1157
+ import_chalk3.default.yellow.bold(" \u{1F680} Keep this site audited, free"),
1158
+ import_chalk3.default.dim(" A free account audits one site weekly, keeps the history and"),
1159
+ import_chalk3.default.dim(" emails you a weekly report."),
1157
1160
  "",
1158
1161
  import_chalk3.default.cyan(` \u2192 rankcli login`),
1159
1162
  import_chalk3.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1160
1163
  ""
1161
1164
  ],
1162
- // When checks limit is reached
1163
- checks_limit: (ctx) => [
1164
- "",
1165
- import_chalk3.default.yellow(`\u26A0\uFE0F Running ${ctx.current || 50} of ${TIER_LIMITS.solo.checks} available checks`),
1166
- import_chalk3.default.dim(` Unlock all ${TIER_LIMITS.solo.checks}+ checks with Solo ($9/mo)`),
1167
- import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_checks`),
1168
- ""
1169
- ],
1170
1165
  // When domain limit is reached
1171
1166
  domain_limit: (ctx) => [
1172
1167
  "",
@@ -1179,18 +1174,6 @@ var UPSELL_MESSAGES = {
1179
1174
  import_chalk3.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"),
1180
1175
  ""
1181
1176
  ],
1182
- // When GEO features are limited
1183
- geo_limited: (ctx) => [
1184
- "",
1185
- import_chalk3.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1186
- import_chalk3.default.yellow.bold(" \u{1F916} Full GEO analysis requires Solo"),
1187
- import_chalk3.default.dim(" You're seeing basic AI crawler access checks."),
1188
- import_chalk3.default.dim(" Full analysis includes: LLM signals, citation readiness, content structure"),
1189
- "",
1190
- import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_geo`),
1191
- import_chalk3.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1192
- ""
1193
- ],
1194
1177
  // When auto-fix is attempted without access
1195
1178
  auto_fix_blocked: () => [
1196
1179
  "",
@@ -1291,10 +1274,6 @@ function showUpsell(messageKey, context) {
1291
1274
  const lines = messageFn(ctx);
1292
1275
  lines.forEach((line) => console.log(line));
1293
1276
  }
1294
- function hasFeature(tier, feature) {
1295
- const limits = TIER_LIMITS[tier];
1296
- return limits?.features?.[feature] ?? false;
1297
- }
1298
1277
 
1299
1278
  // src/wasm-engine.ts
1300
1279
  var vm = __toESM(require("vm"));
@@ -1474,6 +1453,177 @@ var TOGETHER_DEFAULT = "openai/gpt-oss-120b";
1474
1453
  var FIREWORKS_DEFAULT = "accounts/fireworks/models/gpt-oss-120b";
1475
1454
  var CEREBRAS_DEFAULT = "gpt-oss-120b";
1476
1455
 
1456
+ // src/lib/apple-ai.ts
1457
+ var import_node_child_process = require("child_process");
1458
+ var import_node_crypto2 = require("crypto");
1459
+ var import_node_fs = require("fs");
1460
+ var import_node_os = require("os");
1461
+ var import_node_path = require("path");
1462
+
1463
+ // src/lib/apple-ai-source.ts
1464
+ var APPLE_AI_SWIFT_SOURCE = String.raw`import Foundation
1465
+ import FoundationModels
1466
+
1467
+ func reply(_ object: [String: Any], code: Int32 = 0) -> Never {
1468
+ let data = (try? JSONSerialization.data(withJSONObject: object)) ?? Data("{\"error\":\"could not encode the reply\"}".utf8)
1469
+ FileHandle.standardOutput.write(data)
1470
+ exit(code)
1471
+ }
1472
+
1473
+ let model = SystemLanguageModel.default
1474
+
1475
+ if CommandLine.arguments.contains("--check") {
1476
+ switch model.availability {
1477
+ case .available:
1478
+ reply(["available": true])
1479
+ case .unavailable(let reason):
1480
+ switch reason {
1481
+ case .deviceNotEligible: reply(["available": false, "reason": "deviceNotEligible"])
1482
+ case .appleIntelligenceNotEnabled: reply(["available": false, "reason": "appleIntelligenceNotEnabled"])
1483
+ case .modelNotReady: reply(["available": false, "reason": "modelNotReady"])
1484
+ @unknown default: reply(["available": false, "reason": "unavailable"])
1485
+ }
1486
+ @unknown default:
1487
+ reply(["available": false, "reason": "unavailable"])
1488
+ }
1489
+ }
1490
+
1491
+ struct Request: Decodable {
1492
+ let system: String?
1493
+ let prompt: String
1494
+ let maxTokens: Int?
1495
+ }
1496
+
1497
+ let input = FileHandle.standardInput.readDataToEndOfFile()
1498
+ guard let request = try? JSONDecoder().decode(Request.self, from: input) else {
1499
+ reply(["error": "expected a JSON request on stdin"], code: 2)
1500
+ }
1501
+
1502
+ let session: LanguageModelSession
1503
+ if let system = request.system {
1504
+ session = LanguageModelSession(instructions: system)
1505
+ } else {
1506
+ session = LanguageModelSession()
1507
+ }
1508
+
1509
+ do {
1510
+ let response = try await session.respond(
1511
+ to: request.prompt,
1512
+ options: GenerationOptions(maximumResponseTokens: request.maxTokens)
1513
+ )
1514
+ reply(["text": response.content])
1515
+ } catch let error as LanguageModelSession.GenerationError {
1516
+ switch error {
1517
+ case .exceededContextWindowSize:
1518
+ reply(["error": "the prompt is too long for the on-device model"], code: 1)
1519
+ case .guardrailViolation:
1520
+ reply(["error": "the on-device model's guardrails declined the request"], code: 1)
1521
+ case .assetsUnavailable:
1522
+ reply(["error": "the on-device model isn't downloaded yet"], code: 1)
1523
+ default:
1524
+ reply(["error": String(describing: error)], code: 1)
1525
+ }
1526
+ } catch {
1527
+ reply(["error": String(describing: error)], code: 1)
1528
+ }
1529
+ `;
1530
+
1531
+ // src/lib/apple-ai.ts
1532
+ function isAppleIntelligenceCapable(platform = process.platform, arch = process.arch, darwinRelease = (0, import_node_os.release)()) {
1533
+ return platform === "darwin" && arch === "arm64" && Number(darwinRelease.split(".")[0]) >= 25;
1534
+ }
1535
+ var REASONS = {
1536
+ deviceNotEligible: "this Mac doesn't support Apple Intelligence",
1537
+ appleIntelligenceNotEnabled: "Apple Intelligence is turned off (System Settings > Apple Intelligence & Siri)",
1538
+ modelNotReady: "the on-device model is still downloading"
1539
+ };
1540
+ function run(file, args, opts) {
1541
+ return new Promise((resolve) => {
1542
+ const child = (0, import_node_child_process.spawn)(file, args, { stdio: ["pipe", "pipe", "pipe"] });
1543
+ let stdout = "";
1544
+ let stderr = "";
1545
+ let timedOut = false;
1546
+ const timer = setTimeout(() => {
1547
+ timedOut = true;
1548
+ child.kill("SIGKILL");
1549
+ }, opts.timeoutMs);
1550
+ child.stdout.on("data", (d) => stdout += d);
1551
+ child.stderr.on("data", (d) => stderr += d);
1552
+ child.on("error", (error) => {
1553
+ clearTimeout(timer);
1554
+ resolve({ code: null, stdout, stderr: stderr || error.message, timedOut });
1555
+ });
1556
+ child.on("close", (code) => {
1557
+ clearTimeout(timer);
1558
+ resolve({ code, stdout, stderr, timedOut });
1559
+ });
1560
+ child.stdin.on("error", () => {
1561
+ });
1562
+ child.stdin.end(opts.input ?? "");
1563
+ });
1564
+ }
1565
+ function hasDeveloperTools() {
1566
+ return new Promise((resolve) => (0, import_node_child_process.execFile)("xcode-select", ["-p"], (error) => resolve(!error)));
1567
+ }
1568
+ var helperPromise = null;
1569
+ function ensureHelper(env) {
1570
+ if (env.RANKCLI_APPLE_AI_HELPER) return Promise.resolve({ helper: env.RANKCLI_APPLE_AI_HELPER });
1571
+ helperPromise ??= (async () => {
1572
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(APPLE_AI_SWIFT_SOURCE).digest("hex").slice(0, 12);
1573
+ const dir = (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Caches", "rankcli", `apple-ai-${hash}`);
1574
+ const helper = (0, import_node_path.join)(dir, "rankcli-apple-ai");
1575
+ if ((0, import_node_fs.existsSync)(helper)) return { helper };
1576
+ const failed = (0, import_node_path.join)(dir, "build-failed");
1577
+ if ((0, import_node_fs.existsSync)(failed) && Date.now() - (0, import_node_fs.statSync)(failed).mtimeMs < 7 * 24 * 60 * 60 * 1e3) {
1578
+ return { error: (0, import_node_fs.readFileSync)(failed, "utf8") };
1579
+ }
1580
+ if (!await hasDeveloperTools()) {
1581
+ return { error: "using it needs the Xcode command-line tools (xcode-select --install)" };
1582
+ }
1583
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
1584
+ const source = (0, import_node_path.join)(dir, "main.swift");
1585
+ (0, import_node_fs.writeFileSync)(source, APPLE_AI_SWIFT_SOURCE);
1586
+ process.stderr.write("Preparing Apple Intelligence for RankCLI (one-time build of a small helper)...\n");
1587
+ const tmp = `${helper}.${process.pid}`;
1588
+ const build = await run("xcrun", ["swiftc", "-O", "-target", "arm64-apple-macos26.0", source, "-o", tmp], { timeoutMs: 18e4 });
1589
+ if (build.code !== 0 || !(0, import_node_fs.existsSync)(tmp)) {
1590
+ const firstError = build.stderr.split("\n").find((line) => /error:/.test(line))?.trim();
1591
+ const error = `the helper didn't build${firstError ? ` (${firstError})` : ""} - it needs the macOS 26 SDK (update Xcode or its command-line tools)`;
1592
+ (0, import_node_fs.writeFileSync)(failed, error);
1593
+ return { error };
1594
+ }
1595
+ (0, import_node_fs.renameSync)(tmp, helper);
1596
+ return { helper };
1597
+ })();
1598
+ return helperPromise;
1599
+ }
1600
+ async function checkAppleAI(env = process.env) {
1601
+ if (!env.RANKCLI_APPLE_AI_HELPER && !isAppleIntelligenceCapable()) {
1602
+ return { available: false, reason: "it needs macOS 26 or later on Apple silicon" };
1603
+ }
1604
+ const ensured = await ensureHelper(env);
1605
+ if ("error" in ensured) return { available: false, reason: ensured.error };
1606
+ const check = await run(ensured.helper, ["--check"], { timeoutMs: 15e3 });
1607
+ try {
1608
+ const status = JSON.parse(check.stdout);
1609
+ if (status.available) return { available: true, helper: ensured.helper };
1610
+ return { available: false, reason: REASONS[status.reason ?? ""] ?? "Apple Intelligence is unavailable" };
1611
+ } catch {
1612
+ return { available: false, reason: `the helper failed (${(check.stderr || check.stdout).trim().slice(0, 120) || `exit ${check.code}`})` };
1613
+ }
1614
+ }
1615
+ async function generateWithAppleAI(helper, request, timeoutMs = 12e4) {
1616
+ const result = await run(helper, [], { input: JSON.stringify(request), timeoutMs });
1617
+ if (result.timedOut) return { error: `timed out after ${Math.round(timeoutMs / 1e3)}s` };
1618
+ try {
1619
+ const reply = JSON.parse(result.stdout);
1620
+ if (typeof reply.text === "string") return { text: reply.text };
1621
+ return { error: reply.error || "no reply" };
1622
+ } catch {
1623
+ return { error: (result.stderr || result.stdout).trim().slice(0, 200) || `exit ${result.code}` };
1624
+ }
1625
+ }
1626
+
1477
1627
  // src/lib/ai.ts
1478
1628
  var AI_PROVIDERS = {
1479
1629
  // Hosted
@@ -1488,6 +1638,7 @@ var AI_PROVIDERS = {
1488
1638
  together: { label: "Together", baseUrl: "https://api.together.xyz/v1", model: TOGETHER_DEFAULT, envKeys: ["TOGETHER_API_KEY"] },
1489
1639
  fireworks: { label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", model: FIREWORKS_DEFAULT, envKeys: ["FIREWORKS_API_KEY"] },
1490
1640
  // Local / self-hosted
1641
+ apple: { label: "Apple Intelligence", model: "on-device", envKeys: [], local: true },
1491
1642
  ollama: { label: "Ollama", baseUrl: "http://localhost:11434/v1", envKeys: [], local: true },
1492
1643
  lmstudio: { label: "LM Studio", baseUrl: "http://localhost:1234/v1", envKeys: [], local: true },
1493
1644
  llamacpp: { label: "llama.cpp", baseUrl: "http://localhost:8080/v1", envKeys: [], local: true },
@@ -1552,28 +1703,62 @@ async function firstServerModel(baseUrl, apiKey) {
1552
1703
  }
1553
1704
  function noProviderHints() {
1554
1705
  return [
1706
+ "On a Mac with Apple Intelligence (macOS 26+), the on-device model is used automatically",
1555
1707
  `Set one of ${AI_ENV_KEYS.join(", ")}, or pass --ai-key`,
1556
1708
  "Or run a local model: start Ollama (https://ollama.com) and `ollama pull llama3.2` - it is picked up automatically",
1557
1709
  "Or point at any OpenAI-compatible server: --ai-base-url http://localhost:8080/v1 --ai-model <name>"
1558
1710
  ];
1559
1711
  }
1560
- async function resolveAIProvider(opts, env = process.env, probe = probeOllama) {
1561
- let provider = opts.aiProvider?.trim().toLowerCase();
1712
+ function appleResolution(status, notice) {
1713
+ const preset = AI_PROVIDERS.apple;
1714
+ return {
1715
+ ok: true,
1716
+ ai: {
1717
+ provider: "apple",
1718
+ label: preset.label,
1719
+ baseUrl: "",
1720
+ model: preset.model,
1721
+ jsonMode: false,
1722
+ anthropic: false,
1723
+ local: true,
1724
+ helper: status.helper,
1725
+ notice
1726
+ }
1727
+ };
1728
+ }
1729
+ async function resolveAIProvider(opts, env = process.env, probe = probeOllama, probeApple = checkAppleAI) {
1730
+ const requested = opts.aiProvider?.trim() || (!opts.aiKey && !opts.aiBaseUrl ? env.RANKCLI_AI_PROVIDER?.trim() : void 0);
1731
+ let provider = requested?.toLowerCase();
1562
1732
  let notice;
1563
1733
  let ollamaModels = null;
1564
1734
  if (provider && !AI_PROVIDERS[provider]) {
1565
1735
  return {
1566
1736
  ok: false,
1567
- error: `Unknown AI provider "${opts.aiProvider}"`,
1737
+ error: `Unknown AI provider "${requested}"`,
1568
1738
  hints: [`Choose one of: ${AI_PROVIDER_IDS.join(", ")}`]
1569
1739
  };
1570
1740
  }
1741
+ if (provider === "apple") {
1742
+ const apple = await probeApple(env);
1743
+ if (!apple.available) {
1744
+ return {
1745
+ ok: false,
1746
+ error: `Apple Intelligence isn't available: ${apple.reason}`,
1747
+ hints: ["Choose another provider with --ai-provider (e.g. openrouter, ollama)"]
1748
+ };
1749
+ }
1750
+ return appleResolution(apple);
1751
+ }
1571
1752
  if (!provider) {
1572
1753
  if (opts.aiBaseUrl) {
1573
1754
  provider = "custom";
1574
1755
  } else if (opts.aiKey) {
1575
1756
  provider = providerFromKey(opts.aiKey) ?? "openai";
1576
1757
  } else {
1758
+ const apple = await probeApple(env);
1759
+ if (apple.available) {
1760
+ return appleResolution(apple, "Using Apple Intelligence on this Mac (on-device, free) - pass --ai-provider to use another");
1761
+ }
1577
1762
  provider = DETECT_ORDER.find((id) => AI_PROVIDERS[id].envKeys.some((k) => env[k]));
1578
1763
  }
1579
1764
  }
@@ -1713,9 +1898,11 @@ function normalizeAnalysis(raw) {
1713
1898
  }
1714
1899
  var SEVERITY_ORDER = { critical: 0, error: 1, warning: 2, notice: 3, info: 3 };
1715
1900
  var MAX_ISSUES_IN_PROMPT = 60;
1716
- function buildPrompt(url, issues) {
1901
+ var APPLE_MAX_ISSUES = 25;
1902
+ var APPLE_MAX_TOKENS = 1200;
1903
+ function buildPrompt(url, issues, maxIssues = MAX_ISSUES_IN_PROMPT) {
1717
1904
  const ranked = [...issues].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4));
1718
- const shown = ranked.slice(0, MAX_ISSUES_IN_PROMPT);
1905
+ const shown = ranked.slice(0, maxIssues);
1719
1906
  const more = ranked.length > shown.length ? `
1720
1907
  (${ranked.length - shown.length} lower-severity issues omitted)` : "";
1721
1908
  return `Analyze these SEO audit results and provide prioritized recommendations:
@@ -1735,8 +1922,17 @@ Respond with only this JSON object - no prose, no code fences:
1735
1922
  }`;
1736
1923
  }
1737
1924
  async function runAIAnalysis(url, issues, ai) {
1738
- const prompt = buildPrompt(url, issues);
1739
1925
  const system = "You are an SEO expert. Respond with valid JSON only.";
1926
+ if (ai.provider === "apple" && ai.helper) {
1927
+ const reply = await generateWithAppleAI(ai.helper, {
1928
+ system,
1929
+ prompt: buildPrompt(url, issues, APPLE_MAX_ISSUES),
1930
+ maxTokens: APPLE_MAX_TOKENS
1931
+ });
1932
+ if ("error" in reply) return { error: `${ai.label} request failed: ${reply.error}` };
1933
+ return parseAnalysis(reply.text, ai);
1934
+ }
1935
+ const prompt = buildPrompt(url, issues);
1740
1936
  const timeoutMs = ai.local ? 3e5 : 9e4;
1741
1937
  const maxTokens = 4096;
1742
1938
  const headers = { "Content-Type": "application/json" };
@@ -1786,6 +1982,9 @@ async function runAIAnalysis(url, issues, ai) {
1786
1982
  if (!content) {
1787
1983
  return { error: `${ai.label} returned no content (a reasoning model may have used its whole token budget - try another --ai-model)` };
1788
1984
  }
1985
+ return parseAnalysis(content, ai);
1986
+ }
1987
+ function parseAnalysis(content, ai) {
1789
1988
  const parsed = extractJsonObject(content);
1790
1989
  const analysis = parsed ? normalizeAnalysis(parsed) : null;
1791
1990
  if (!analysis) {
@@ -1991,7 +2190,7 @@ async function audit(options) {
1991
2190
  const tierId = tierFromPlanId(planId) ?? (loggedIn ? "free" : "free_anon");
1992
2191
  const checksLimit = TIER_LIMITS[tierId].checks;
1993
2192
  let premiumChecks;
1994
- if (checksLimit > TIER_LIMITS.free.checks) {
2193
+ {
1995
2194
  const passphrase = getDecryptPassphrase();
1996
2195
  if (passphrase) {
1997
2196
  try {
@@ -2202,17 +2401,6 @@ async function audit(options) {
2202
2401
  }
2203
2402
  }
2204
2403
  console.log("\n" + import_chalk4.default.bold("\u2550".repeat(60)));
2205
- const isFreeTier = tierId === "free" || tierId === "free_anon";
2206
- if (isFreeTier) {
2207
- const checksRun = mergedIssues.length > 0 ? loggedIn ? 100 : 50 : 0;
2208
- showUpsell("checks_limit", {
2209
- tier: loggedIn ? "free" : "free_anon",
2210
- current: checksRun
2211
- });
2212
- }
2213
- if (options.geo && isFreeTier) {
2214
- showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
2215
- }
2216
2404
  if (loggedIn) {
2217
2405
  const syncSpinner = (0, import_ora2.default)("Syncing to cloud...").start();
2218
2406
  const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
@@ -2326,14 +2514,6 @@ async function apply(options) {
2326
2514
  }
2327
2515
  console.log("\n\u{1F680} RankCLI - Apply Fixes");
2328
2516
  console.log("\u2550".repeat(50));
2329
- const loggedIn = isLoggedIn();
2330
- const userInfo = getUserInfo();
2331
- const tier = userInfo.planName?.toLowerCase() || "free";
2332
- const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
2333
- if (!hasAutoFix) {
2334
- showUpsell("auto_fix_blocked");
2335
- return;
2336
- }
2337
2517
  console.log("\n\u{1F50D} Detecting framework...");
2338
2518
  const frameworkResult = await (0, import_agent_runtime3.detectFramework)({ cwd });
2339
2519
  const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
@@ -2589,16 +2769,6 @@ async function runFix(options) {
2589
2769
  if (loggedIn) await ensureApiKeyPlan();
2590
2770
  const userInfo = getUserInfo();
2591
2771
  const tier = tierFromPlanId(userInfo.planId) ?? tierFromPlanId(userInfo.planName) ?? "free";
2592
- const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
2593
- if (!options.dryRun && !hasAutoFix) {
2594
- if (options.output !== "json") {
2595
- showUpsell("auto_fix_blocked");
2596
- }
2597
- return {
2598
- success: false,
2599
- error: "Auto-fix requires Solo tier or higher. Please upgrade at " + WEB_APP_URL + "/account"
2600
- };
2601
- }
2602
2772
  if (options.output !== "json") {
2603
2773
  console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
2604
2774
  console.log("\u2550".repeat(50));
@@ -3917,7 +4087,7 @@ async function alerts(options) {
3917
4087
  if (options.list) {
3918
4088
  console.log("\u{1F4CB} Current alert configurations:\n");
3919
4089
  console.log(" (Alert configs are stored in your RankCLI account)");
3920
- console.log(" Run `rankcli auth login` first, then manage alerts at https://rankcli.dev/account/alerts\n");
4090
+ console.log(" Run `rankcli login` first, then manage alerts at https://rankcli.dev/account?tab=notifications\n");
3921
4091
  rl.close();
3922
4092
  return;
3923
4093
  }
@@ -3992,8 +4162,8 @@ async function alerts(options) {
3992
4162
  }
3993
4163
  console.log("");
3994
4164
  console.log("To save this configuration:");
3995
- console.log("1. Log in with `rankcli auth login`");
3996
- console.log("2. Add alert config at https://rankcli.dev/account/alerts");
4165
+ console.log("1. Log in with `rankcli login`");
4166
+ console.log("2. Add alert config at https://rankcli.dev/account?tab=notifications");
3997
4167
  console.log("");
3998
4168
  console.log("Or use the API:");
3999
4169
  console.log(` POST /api/alerts with webhook_url and settings`);
@@ -4052,7 +4222,7 @@ async function testWebhook(channel, webhookUrl) {
4052
4222
  }
4053
4223
 
4054
4224
  // src/version.ts
4055
- var VERSION = "0.0.30";
4225
+ var VERSION = "0.0.32";
4056
4226
 
4057
4227
  // src/index.ts
4058
4228
  var program = new import_commander.Command();
@@ -4064,7 +4234,7 @@ program.command("logout").description("Log out of your RankCLI account").action(
4064
4234
  program.command("whoami").description("Show current logged in user").action(whoami);
4065
4235
  program.command("init").description("Set up RankCLI in your project (detects framework, git/GitHub, AI keys)").option("-y, --yes", "Skip prompts and use defaults").action(init);
4066
4236
  program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
4067
- 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("--fail-on <level>", "CI gate: exit 1 if any issue is at or above this severity (error, warning, notice)").option("--min-score <n>", "CI gate: exit 1 if the overall score is below n (0-100)").option("--ai", "Enable AI analysis (your own key, or a local model)").option("--ai-provider <provider>", "AI provider (auto-detected if omitted): openrouter, openai, anthropic, groq, gemini, deepseek, mistral, cerebras, together, fireworks, ollama, lmstudio, llamacpp, vllm, custom").option("--ai-key <key>", "AI provider API key (or set the provider's env var, e.g. OPENROUTER_API_KEY)").option("--ai-model <model>", "Model id (defaults to a cheap model per provider, or the local server's loaded model)").option("--ai-base-url <url>", "OpenAI-compatible base URL, e.g. http://localhost:8080/v1 (implies --ai-provider custom)").addHelpText("after", AUDIT_HELP_TEXT).action(async (options) => {
4237
+ program.command("audit").description("Run an SEO + GEO audit (280+ checks)").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("--fail-on <level>", "CI gate: exit 1 if any issue is at or above this severity (error, warning, notice)").option("--min-score <n>", "CI gate: exit 1 if the overall score is below n (0-100)").option("--ai", "Enable AI analysis (your own key, or a local model)").option("--ai-provider <provider>", "AI provider (auto-detected if omitted; Apple Intelligence on a Mac that has it, or set RANKCLI_AI_PROVIDER): apple, openrouter, openai, anthropic, groq, gemini, deepseek, mistral, cerebras, together, fireworks, ollama, lmstudio, llamacpp, vllm, custom").option("--ai-key <key>", "AI provider API key (or set the provider's env var, e.g. OPENROUTER_API_KEY)").option("--ai-model <model>", "Model id (defaults to a cheap model per provider, or the local server's loaded model)").option("--ai-base-url <url>", "OpenAI-compatible base URL, e.g. http://localhost:8080/v1 (implies --ai-provider custom)").addHelpText("after", AUDIT_HELP_TEXT).action(async (options) => {
4068
4238
  process.exitCode = await audit(options);
4069
4239
  });
4070
4240
  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);
package/dist/index.mjs CHANGED
@@ -26,19 +26,19 @@ import { createHash } from "crypto";
26
26
  import { createClient } from "@supabase/supabase-js";
27
27
  const INJECTED_CONFIG = {"supabaseUrl":"https://bspljbxwbjiqueeyzyat.supabase.co","supabaseAnonKey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJzcGxqYnh3YmppcXVlZXl6eWF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNzQ1NTksImV4cCI6MjA4NDg1MDU1OX0.7gprxM2gT9cZ99sjZURLltrln78dEnwyFFKYpTzBaUU","webAppUrl":"https://rankcli.dev","decryptPassphrase":"lKFLeIw/xGPa6E8c4XswZX/oiLtJf/yLF/Y6zwovte8="};
28
28
  function getSupabaseUrl() {
29
- const url = process.env.RANKCLI_SUPABASE_URL || process.env.VITE_SUPABASE_URL || INJECTED_CONFIG?.supabaseUrl;
29
+ const url = process.env.RANKCLI_SUPABASE_URL || INJECTED_CONFIG?.supabaseUrl;
30
30
  if (!url) {
31
31
  throw new Error(
32
- "Supabase URL not configured. Set RANKCLI_SUPABASE_URL environment variable.\nSee: https://rankcli.dev/docs/cli-setup"
32
+ "Supabase URL not configured. Set RANKCLI_SUPABASE_URL environment variable.\nSee: https://rankcli.dev/docs#getting-started"
33
33
  );
34
34
  }
35
35
  return url;
36
36
  }
37
37
  function getSupabaseAnonKey() {
38
- const key = process.env.RANKCLI_SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || INJECTED_CONFIG?.supabaseAnonKey;
38
+ const key = process.env.RANKCLI_SUPABASE_ANON_KEY || INJECTED_CONFIG?.supabaseAnonKey;
39
39
  if (!key) {
40
40
  throw new Error(
41
- "Supabase anon key not configured. Set RANKCLI_SUPABASE_ANON_KEY environment variable.\nSee: https://rankcli.dev/docs/cli-setup"
41
+ "Supabase anon key not configured. Set RANKCLI_SUPABASE_ANON_KEY environment variable.\nSee: https://rankcli.dev/docs#getting-started"
42
42
  );
43
43
  }
44
44
  return key;
@@ -1097,23 +1097,25 @@ import chalk4 from "chalk";
1097
1097
  import chalk3 from "chalk";
1098
1098
  var WEB_APP_URL2 = "https://rankcli.dev";
1099
1099
  var TIER_LIMITS = {
1100
+ // Every plan runs every check (pricing-tiers.json: free_audit / free_registered 280). Plans
1101
+ // differ in sites, schedules, pages per audit, PRs and alerts - not in what an audit checks.
1100
1102
  free_anon: {
1101
- checks: 50,
1103
+ checks: 280,
1102
1104
  sites: 0,
1103
1105
  domains_per_month: 1,
1104
- features: { geo: false, auto_fix: false, github: false, ai: false }
1106
+ features: { geo: true, auto_fix: true, github: false, ai: false }
1105
1107
  },
1106
1108
  free: {
1107
- checks: 100,
1109
+ checks: 280,
1108
1110
  sites: 1,
1109
1111
  domains_per_month: 2,
1110
- features: { geo: "basic", auto_fix: false, github: false, ai: false }
1112
+ features: { geo: true, auto_fix: true, github: true, ai: false }
1111
1113
  },
1112
1114
  solo: {
1113
1115
  checks: 280,
1114
1116
  sites: 3,
1115
1117
  domains_per_month: 5,
1116
- features: { geo: true, auto_fix: true, github: false, ai: true }
1118
+ features: { geo: true, auto_fix: true, github: true, ai: true }
1117
1119
  },
1118
1120
  pro: {
1119
1121
  checks: 280,
@@ -1139,21 +1141,14 @@ var UPSELL_MESSAGES = {
1139
1141
  free_audit_complete: () => [
1140
1142
  "",
1141
1143
  chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1142
- chalk3.yellow.bold(" \u{1F680} Get 2x more checks + audit history"),
1143
- chalk3.dim(" Create a free account to unlock 100 checks and save your results."),
1144
+ chalk3.yellow.bold(" \u{1F680} Keep this site audited, free"),
1145
+ chalk3.dim(" A free account audits one site weekly, keeps the history and"),
1146
+ chalk3.dim(" emails you a weekly report."),
1144
1147
  "",
1145
1148
  chalk3.cyan(` \u2192 rankcli login`),
1146
1149
  chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1147
1150
  ""
1148
1151
  ],
1149
- // When checks limit is reached
1150
- checks_limit: (ctx) => [
1151
- "",
1152
- chalk3.yellow(`\u26A0\uFE0F Running ${ctx.current || 50} of ${TIER_LIMITS.solo.checks} available checks`),
1153
- chalk3.dim(` Unlock all ${TIER_LIMITS.solo.checks}+ checks with Solo ($9/mo)`),
1154
- chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_checks`),
1155
- ""
1156
- ],
1157
1152
  // When domain limit is reached
1158
1153
  domain_limit: (ctx) => [
1159
1154
  "",
@@ -1166,18 +1161,6 @@ var UPSELL_MESSAGES = {
1166
1161
  chalk3.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1167
1162
  ""
1168
1163
  ],
1169
- // When GEO features are limited
1170
- geo_limited: (ctx) => [
1171
- "",
1172
- chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1173
- chalk3.yellow.bold(" \u{1F916} Full GEO analysis requires Solo"),
1174
- chalk3.dim(" You're seeing basic AI crawler access checks."),
1175
- chalk3.dim(" Full analysis includes: LLM signals, citation readiness, content structure"),
1176
- "",
1177
- chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_geo`),
1178
- chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
1179
- ""
1180
- ],
1181
1164
  // When auto-fix is attempted without access
1182
1165
  auto_fix_blocked: () => [
1183
1166
  "",
@@ -1278,10 +1261,6 @@ function showUpsell(messageKey, context) {
1278
1261
  const lines = messageFn(ctx);
1279
1262
  lines.forEach((line) => console.log(line));
1280
1263
  }
1281
- function hasFeature(tier, feature) {
1282
- const limits = TIER_LIMITS[tier];
1283
- return limits?.features?.[feature] ?? false;
1284
- }
1285
1264
 
1286
1265
  // src/models.ts
1287
1266
  var OPENAI_FAST = "gpt-5.6-luna";
@@ -1295,6 +1274,177 @@ var TOGETHER_DEFAULT = "openai/gpt-oss-120b";
1295
1274
  var FIREWORKS_DEFAULT = "accounts/fireworks/models/gpt-oss-120b";
1296
1275
  var CEREBRAS_DEFAULT = "gpt-oss-120b";
1297
1276
 
1277
+ // src/lib/apple-ai.ts
1278
+ import { execFile, spawn } from "child_process";
1279
+ import { createHash as createHash2 } from "crypto";
1280
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, renameSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
1281
+ import { homedir, release } from "os";
1282
+ import { join as join4 } from "path";
1283
+
1284
+ // src/lib/apple-ai-source.ts
1285
+ var APPLE_AI_SWIFT_SOURCE = String.raw`import Foundation
1286
+ import FoundationModels
1287
+
1288
+ func reply(_ object: [String: Any], code: Int32 = 0) -> Never {
1289
+ let data = (try? JSONSerialization.data(withJSONObject: object)) ?? Data("{\"error\":\"could not encode the reply\"}".utf8)
1290
+ FileHandle.standardOutput.write(data)
1291
+ exit(code)
1292
+ }
1293
+
1294
+ let model = SystemLanguageModel.default
1295
+
1296
+ if CommandLine.arguments.contains("--check") {
1297
+ switch model.availability {
1298
+ case .available:
1299
+ reply(["available": true])
1300
+ case .unavailable(let reason):
1301
+ switch reason {
1302
+ case .deviceNotEligible: reply(["available": false, "reason": "deviceNotEligible"])
1303
+ case .appleIntelligenceNotEnabled: reply(["available": false, "reason": "appleIntelligenceNotEnabled"])
1304
+ case .modelNotReady: reply(["available": false, "reason": "modelNotReady"])
1305
+ @unknown default: reply(["available": false, "reason": "unavailable"])
1306
+ }
1307
+ @unknown default:
1308
+ reply(["available": false, "reason": "unavailable"])
1309
+ }
1310
+ }
1311
+
1312
+ struct Request: Decodable {
1313
+ let system: String?
1314
+ let prompt: String
1315
+ let maxTokens: Int?
1316
+ }
1317
+
1318
+ let input = FileHandle.standardInput.readDataToEndOfFile()
1319
+ guard let request = try? JSONDecoder().decode(Request.self, from: input) else {
1320
+ reply(["error": "expected a JSON request on stdin"], code: 2)
1321
+ }
1322
+
1323
+ let session: LanguageModelSession
1324
+ if let system = request.system {
1325
+ session = LanguageModelSession(instructions: system)
1326
+ } else {
1327
+ session = LanguageModelSession()
1328
+ }
1329
+
1330
+ do {
1331
+ let response = try await session.respond(
1332
+ to: request.prompt,
1333
+ options: GenerationOptions(maximumResponseTokens: request.maxTokens)
1334
+ )
1335
+ reply(["text": response.content])
1336
+ } catch let error as LanguageModelSession.GenerationError {
1337
+ switch error {
1338
+ case .exceededContextWindowSize:
1339
+ reply(["error": "the prompt is too long for the on-device model"], code: 1)
1340
+ case .guardrailViolation:
1341
+ reply(["error": "the on-device model's guardrails declined the request"], code: 1)
1342
+ case .assetsUnavailable:
1343
+ reply(["error": "the on-device model isn't downloaded yet"], code: 1)
1344
+ default:
1345
+ reply(["error": String(describing: error)], code: 1)
1346
+ }
1347
+ } catch {
1348
+ reply(["error": String(describing: error)], code: 1)
1349
+ }
1350
+ `;
1351
+
1352
+ // src/lib/apple-ai.ts
1353
+ function isAppleIntelligenceCapable(platform = process.platform, arch = process.arch, darwinRelease = release()) {
1354
+ return platform === "darwin" && arch === "arm64" && Number(darwinRelease.split(".")[0]) >= 25;
1355
+ }
1356
+ var REASONS = {
1357
+ deviceNotEligible: "this Mac doesn't support Apple Intelligence",
1358
+ appleIntelligenceNotEnabled: "Apple Intelligence is turned off (System Settings > Apple Intelligence & Siri)",
1359
+ modelNotReady: "the on-device model is still downloading"
1360
+ };
1361
+ function run(file, args, opts) {
1362
+ return new Promise((resolve) => {
1363
+ const child = spawn(file, args, { stdio: ["pipe", "pipe", "pipe"] });
1364
+ let stdout = "";
1365
+ let stderr = "";
1366
+ let timedOut = false;
1367
+ const timer = setTimeout(() => {
1368
+ timedOut = true;
1369
+ child.kill("SIGKILL");
1370
+ }, opts.timeoutMs);
1371
+ child.stdout.on("data", (d) => stdout += d);
1372
+ child.stderr.on("data", (d) => stderr += d);
1373
+ child.on("error", (error) => {
1374
+ clearTimeout(timer);
1375
+ resolve({ code: null, stdout, stderr: stderr || error.message, timedOut });
1376
+ });
1377
+ child.on("close", (code) => {
1378
+ clearTimeout(timer);
1379
+ resolve({ code, stdout, stderr, timedOut });
1380
+ });
1381
+ child.stdin.on("error", () => {
1382
+ });
1383
+ child.stdin.end(opts.input ?? "");
1384
+ });
1385
+ }
1386
+ function hasDeveloperTools() {
1387
+ return new Promise((resolve) => execFile("xcode-select", ["-p"], (error) => resolve(!error)));
1388
+ }
1389
+ var helperPromise = null;
1390
+ function ensureHelper(env) {
1391
+ if (env.RANKCLI_APPLE_AI_HELPER) return Promise.resolve({ helper: env.RANKCLI_APPLE_AI_HELPER });
1392
+ helperPromise ??= (async () => {
1393
+ const hash = createHash2("sha256").update(APPLE_AI_SWIFT_SOURCE).digest("hex").slice(0, 12);
1394
+ const dir = join4(homedir(), "Library", "Caches", "rankcli", `apple-ai-${hash}`);
1395
+ const helper = join4(dir, "rankcli-apple-ai");
1396
+ if (existsSync4(helper)) return { helper };
1397
+ const failed = join4(dir, "build-failed");
1398
+ if (existsSync4(failed) && Date.now() - statSync2(failed).mtimeMs < 7 * 24 * 60 * 60 * 1e3) {
1399
+ return { error: readFileSync4(failed, "utf8") };
1400
+ }
1401
+ if (!await hasDeveloperTools()) {
1402
+ return { error: "using it needs the Xcode command-line tools (xcode-select --install)" };
1403
+ }
1404
+ mkdirSync(dir, { recursive: true });
1405
+ const source = join4(dir, "main.swift");
1406
+ writeFileSync3(source, APPLE_AI_SWIFT_SOURCE);
1407
+ process.stderr.write("Preparing Apple Intelligence for RankCLI (one-time build of a small helper)...\n");
1408
+ const tmp = `${helper}.${process.pid}`;
1409
+ const build = await run("xcrun", ["swiftc", "-O", "-target", "arm64-apple-macos26.0", source, "-o", tmp], { timeoutMs: 18e4 });
1410
+ if (build.code !== 0 || !existsSync4(tmp)) {
1411
+ const firstError = build.stderr.split("\n").find((line) => /error:/.test(line))?.trim();
1412
+ const error = `the helper didn't build${firstError ? ` (${firstError})` : ""} - it needs the macOS 26 SDK (update Xcode or its command-line tools)`;
1413
+ writeFileSync3(failed, error);
1414
+ return { error };
1415
+ }
1416
+ renameSync(tmp, helper);
1417
+ return { helper };
1418
+ })();
1419
+ return helperPromise;
1420
+ }
1421
+ async function checkAppleAI(env = process.env) {
1422
+ if (!env.RANKCLI_APPLE_AI_HELPER && !isAppleIntelligenceCapable()) {
1423
+ return { available: false, reason: "it needs macOS 26 or later on Apple silicon" };
1424
+ }
1425
+ const ensured = await ensureHelper(env);
1426
+ if ("error" in ensured) return { available: false, reason: ensured.error };
1427
+ const check = await run(ensured.helper, ["--check"], { timeoutMs: 15e3 });
1428
+ try {
1429
+ const status = JSON.parse(check.stdout);
1430
+ if (status.available) return { available: true, helper: ensured.helper };
1431
+ return { available: false, reason: REASONS[status.reason ?? ""] ?? "Apple Intelligence is unavailable" };
1432
+ } catch {
1433
+ return { available: false, reason: `the helper failed (${(check.stderr || check.stdout).trim().slice(0, 120) || `exit ${check.code}`})` };
1434
+ }
1435
+ }
1436
+ async function generateWithAppleAI(helper, request, timeoutMs = 12e4) {
1437
+ const result = await run(helper, [], { input: JSON.stringify(request), timeoutMs });
1438
+ if (result.timedOut) return { error: `timed out after ${Math.round(timeoutMs / 1e3)}s` };
1439
+ try {
1440
+ const reply = JSON.parse(result.stdout);
1441
+ if (typeof reply.text === "string") return { text: reply.text };
1442
+ return { error: reply.error || "no reply" };
1443
+ } catch {
1444
+ return { error: (result.stderr || result.stdout).trim().slice(0, 200) || `exit ${result.code}` };
1445
+ }
1446
+ }
1447
+
1298
1448
  // src/lib/ai.ts
1299
1449
  var AI_PROVIDERS = {
1300
1450
  // Hosted
@@ -1309,6 +1459,7 @@ var AI_PROVIDERS = {
1309
1459
  together: { label: "Together", baseUrl: "https://api.together.xyz/v1", model: TOGETHER_DEFAULT, envKeys: ["TOGETHER_API_KEY"] },
1310
1460
  fireworks: { label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", model: FIREWORKS_DEFAULT, envKeys: ["FIREWORKS_API_KEY"] },
1311
1461
  // Local / self-hosted
1462
+ apple: { label: "Apple Intelligence", model: "on-device", envKeys: [], local: true },
1312
1463
  ollama: { label: "Ollama", baseUrl: "http://localhost:11434/v1", envKeys: [], local: true },
1313
1464
  lmstudio: { label: "LM Studio", baseUrl: "http://localhost:1234/v1", envKeys: [], local: true },
1314
1465
  llamacpp: { label: "llama.cpp", baseUrl: "http://localhost:8080/v1", envKeys: [], local: true },
@@ -1373,28 +1524,62 @@ async function firstServerModel(baseUrl, apiKey) {
1373
1524
  }
1374
1525
  function noProviderHints() {
1375
1526
  return [
1527
+ "On a Mac with Apple Intelligence (macOS 26+), the on-device model is used automatically",
1376
1528
  `Set one of ${AI_ENV_KEYS.join(", ")}, or pass --ai-key`,
1377
1529
  "Or run a local model: start Ollama (https://ollama.com) and `ollama pull llama3.2` - it is picked up automatically",
1378
1530
  "Or point at any OpenAI-compatible server: --ai-base-url http://localhost:8080/v1 --ai-model <name>"
1379
1531
  ];
1380
1532
  }
1381
- async function resolveAIProvider(opts, env = process.env, probe = probeOllama) {
1382
- let provider = opts.aiProvider?.trim().toLowerCase();
1533
+ function appleResolution(status, notice) {
1534
+ const preset = AI_PROVIDERS.apple;
1535
+ return {
1536
+ ok: true,
1537
+ ai: {
1538
+ provider: "apple",
1539
+ label: preset.label,
1540
+ baseUrl: "",
1541
+ model: preset.model,
1542
+ jsonMode: false,
1543
+ anthropic: false,
1544
+ local: true,
1545
+ helper: status.helper,
1546
+ notice
1547
+ }
1548
+ };
1549
+ }
1550
+ async function resolveAIProvider(opts, env = process.env, probe = probeOllama, probeApple = checkAppleAI) {
1551
+ const requested = opts.aiProvider?.trim() || (!opts.aiKey && !opts.aiBaseUrl ? env.RANKCLI_AI_PROVIDER?.trim() : void 0);
1552
+ let provider = requested?.toLowerCase();
1383
1553
  let notice;
1384
1554
  let ollamaModels = null;
1385
1555
  if (provider && !AI_PROVIDERS[provider]) {
1386
1556
  return {
1387
1557
  ok: false,
1388
- error: `Unknown AI provider "${opts.aiProvider}"`,
1558
+ error: `Unknown AI provider "${requested}"`,
1389
1559
  hints: [`Choose one of: ${AI_PROVIDER_IDS.join(", ")}`]
1390
1560
  };
1391
1561
  }
1562
+ if (provider === "apple") {
1563
+ const apple = await probeApple(env);
1564
+ if (!apple.available) {
1565
+ return {
1566
+ ok: false,
1567
+ error: `Apple Intelligence isn't available: ${apple.reason}`,
1568
+ hints: ["Choose another provider with --ai-provider (e.g. openrouter, ollama)"]
1569
+ };
1570
+ }
1571
+ return appleResolution(apple);
1572
+ }
1392
1573
  if (!provider) {
1393
1574
  if (opts.aiBaseUrl) {
1394
1575
  provider = "custom";
1395
1576
  } else if (opts.aiKey) {
1396
1577
  provider = providerFromKey(opts.aiKey) ?? "openai";
1397
1578
  } else {
1579
+ const apple = await probeApple(env);
1580
+ if (apple.available) {
1581
+ return appleResolution(apple, "Using Apple Intelligence on this Mac (on-device, free) - pass --ai-provider to use another");
1582
+ }
1398
1583
  provider = DETECT_ORDER.find((id) => AI_PROVIDERS[id].envKeys.some((k) => env[k]));
1399
1584
  }
1400
1585
  }
@@ -1534,9 +1719,11 @@ function normalizeAnalysis(raw) {
1534
1719
  }
1535
1720
  var SEVERITY_ORDER = { critical: 0, error: 1, warning: 2, notice: 3, info: 3 };
1536
1721
  var MAX_ISSUES_IN_PROMPT = 60;
1537
- function buildPrompt(url, issues) {
1722
+ var APPLE_MAX_ISSUES = 25;
1723
+ var APPLE_MAX_TOKENS = 1200;
1724
+ function buildPrompt(url, issues, maxIssues = MAX_ISSUES_IN_PROMPT) {
1538
1725
  const ranked = [...issues].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4));
1539
- const shown = ranked.slice(0, MAX_ISSUES_IN_PROMPT);
1726
+ const shown = ranked.slice(0, maxIssues);
1540
1727
  const more = ranked.length > shown.length ? `
1541
1728
  (${ranked.length - shown.length} lower-severity issues omitted)` : "";
1542
1729
  return `Analyze these SEO audit results and provide prioritized recommendations:
@@ -1556,8 +1743,17 @@ Respond with only this JSON object - no prose, no code fences:
1556
1743
  }`;
1557
1744
  }
1558
1745
  async function runAIAnalysis(url, issues, ai) {
1559
- const prompt = buildPrompt(url, issues);
1560
1746
  const system = "You are an SEO expert. Respond with valid JSON only.";
1747
+ if (ai.provider === "apple" && ai.helper) {
1748
+ const reply = await generateWithAppleAI(ai.helper, {
1749
+ system,
1750
+ prompt: buildPrompt(url, issues, APPLE_MAX_ISSUES),
1751
+ maxTokens: APPLE_MAX_TOKENS
1752
+ });
1753
+ if ("error" in reply) return { error: `${ai.label} request failed: ${reply.error}` };
1754
+ return parseAnalysis(reply.text, ai);
1755
+ }
1756
+ const prompt = buildPrompt(url, issues);
1561
1757
  const timeoutMs = ai.local ? 3e5 : 9e4;
1562
1758
  const maxTokens = 4096;
1563
1759
  const headers = { "Content-Type": "application/json" };
@@ -1607,6 +1803,9 @@ async function runAIAnalysis(url, issues, ai) {
1607
1803
  if (!content) {
1608
1804
  return { error: `${ai.label} returned no content (a reasoning model may have used its whole token budget - try another --ai-model)` };
1609
1805
  }
1806
+ return parseAnalysis(content, ai);
1807
+ }
1808
+ function parseAnalysis(content, ai) {
1610
1809
  const parsed = extractJsonObject(content);
1611
1810
  const analysis = parsed ? normalizeAnalysis(parsed) : null;
1612
1811
  if (!analysis) {
@@ -1812,7 +2011,7 @@ async function audit(options) {
1812
2011
  const tierId = tierFromPlanId(planId) ?? (loggedIn ? "free" : "free_anon");
1813
2012
  const checksLimit = TIER_LIMITS[tierId].checks;
1814
2013
  let premiumChecks;
1815
- if (checksLimit > TIER_LIMITS.free.checks) {
2014
+ {
1816
2015
  const passphrase = getDecryptPassphrase();
1817
2016
  if (passphrase) {
1818
2017
  try {
@@ -2023,17 +2222,6 @@ async function audit(options) {
2023
2222
  }
2024
2223
  }
2025
2224
  console.log("\n" + chalk4.bold("\u2550".repeat(60)));
2026
- const isFreeTier = tierId === "free" || tierId === "free_anon";
2027
- if (isFreeTier) {
2028
- const checksRun = mergedIssues.length > 0 ? loggedIn ? 100 : 50 : 0;
2029
- showUpsell("checks_limit", {
2030
- tier: loggedIn ? "free" : "free_anon",
2031
- current: checksRun
2032
- });
2033
- }
2034
- if (options.geo && isFreeTier) {
2035
- showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
2036
- }
2037
2225
  if (loggedIn) {
2038
2226
  const syncSpinner = ora2("Syncing to cloud...").start();
2039
2227
  const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
@@ -2091,8 +2279,8 @@ Examples:
2091
2279
  `;
2092
2280
 
2093
2281
  // src/commands/apply.ts
2094
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2095
- import { join as join4 } from "path";
2282
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2283
+ import { join as join5 } from "path";
2096
2284
  import {
2097
2285
  runAuditWithFixes,
2098
2286
  generateFixes,
@@ -2126,11 +2314,11 @@ function truncate(str, maxLen) {
2126
2314
  }
2127
2315
  async function apply(options) {
2128
2316
  const cwd = process.cwd();
2129
- const configPath = join4(cwd, "rankcli.config.json");
2317
+ const configPath = join5(cwd, "rankcli.config.json");
2130
2318
  let config2 = null;
2131
- if (existsSync4(configPath)) {
2319
+ if (existsSync5(configPath)) {
2132
2320
  try {
2133
- config2 = JSON.parse(readFileSync4(configPath, "utf-8"));
2321
+ config2 = JSON.parse(readFileSync5(configPath, "utf-8"));
2134
2322
  } catch {
2135
2323
  }
2136
2324
  }
@@ -2152,14 +2340,6 @@ async function apply(options) {
2152
2340
  }
2153
2341
  console.log("\n\u{1F680} RankCLI - Apply Fixes");
2154
2342
  console.log("\u2550".repeat(50));
2155
- const loggedIn = isLoggedIn();
2156
- const userInfo = getUserInfo();
2157
- const tier = userInfo.planName?.toLowerCase() || "free";
2158
- const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
2159
- if (!hasAutoFix) {
2160
- showUpsell("auto_fix_blocked");
2161
- return;
2162
- }
2163
2343
  console.log("\n\u{1F50D} Detecting framework...");
2164
2344
  const frameworkResult = await detectFramework3({ cwd });
2165
2345
  const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
@@ -2262,7 +2442,7 @@ async function apply(options) {
2262
2442
  }
2263
2443
  if (config2 && !config2.url) {
2264
2444
  config2.url = url;
2265
- writeFileSync3(configPath, JSON.stringify(config2, null, 2));
2445
+ writeFileSync4(configPath, JSON.stringify(config2, null, 2));
2266
2446
  console.log("\n\u{1F4BE} Updated config with URL.");
2267
2447
  }
2268
2448
  console.log("\n" + "\u2500".repeat(50));
@@ -2420,16 +2600,6 @@ async function runFix(options) {
2420
2600
  if (loggedIn) await ensureApiKeyPlan();
2421
2601
  const userInfo = getUserInfo();
2422
2602
  const tier = tierFromPlanId(userInfo.planId) ?? tierFromPlanId(userInfo.planName) ?? "free";
2423
- const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
2424
- if (!options.dryRun && !hasAutoFix) {
2425
- if (options.output !== "json") {
2426
- showUpsell("auto_fix_blocked");
2427
- }
2428
- return {
2429
- success: false,
2430
- error: "Auto-fix requires Solo tier or higher. Please upgrade at " + WEB_APP_URL + "/account"
2431
- };
2432
- }
2433
2603
  if (options.output !== "json") {
2434
2604
  console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
2435
2605
  console.log("\u2550".repeat(50));
@@ -3773,7 +3943,7 @@ async function alerts(options) {
3773
3943
  if (options.list) {
3774
3944
  console.log("\u{1F4CB} Current alert configurations:\n");
3775
3945
  console.log(" (Alert configs are stored in your RankCLI account)");
3776
- console.log(" Run `rankcli auth login` first, then manage alerts at https://rankcli.dev/account/alerts\n");
3946
+ console.log(" Run `rankcli login` first, then manage alerts at https://rankcli.dev/account?tab=notifications\n");
3777
3947
  rl.close();
3778
3948
  return;
3779
3949
  }
@@ -3848,8 +4018,8 @@ async function alerts(options) {
3848
4018
  }
3849
4019
  console.log("");
3850
4020
  console.log("To save this configuration:");
3851
- console.log("1. Log in with `rankcli auth login`");
3852
- console.log("2. Add alert config at https://rankcli.dev/account/alerts");
4021
+ console.log("1. Log in with `rankcli login`");
4022
+ console.log("2. Add alert config at https://rankcli.dev/account?tab=notifications");
3853
4023
  console.log("");
3854
4024
  console.log("Or use the API:");
3855
4025
  console.log(` POST /api/alerts with webhook_url and settings`);
@@ -3908,7 +4078,7 @@ async function testWebhook(channel, webhookUrl) {
3908
4078
  }
3909
4079
 
3910
4080
  // src/version.ts
3911
- var VERSION = "0.0.30";
4081
+ var VERSION = "0.0.32";
3912
4082
 
3913
4083
  // src/index.ts
3914
4084
  var program = new Command();
@@ -3920,7 +4090,7 @@ program.command("logout").description("Log out of your RankCLI account").action(
3920
4090
  program.command("whoami").description("Show current logged in user").action(whoami);
3921
4091
  program.command("init").description("Set up RankCLI in your project (detects framework, git/GitHub, AI keys)").option("-y, --yes", "Skip prompts and use defaults").action(init);
3922
4092
  program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
3923
- 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("--fail-on <level>", "CI gate: exit 1 if any issue is at or above this severity (error, warning, notice)").option("--min-score <n>", "CI gate: exit 1 if the overall score is below n (0-100)").option("--ai", "Enable AI analysis (your own key, or a local model)").option("--ai-provider <provider>", "AI provider (auto-detected if omitted): openrouter, openai, anthropic, groq, gemini, deepseek, mistral, cerebras, together, fireworks, ollama, lmstudio, llamacpp, vllm, custom").option("--ai-key <key>", "AI provider API key (or set the provider's env var, e.g. OPENROUTER_API_KEY)").option("--ai-model <model>", "Model id (defaults to a cheap model per provider, or the local server's loaded model)").option("--ai-base-url <url>", "OpenAI-compatible base URL, e.g. http://localhost:8080/v1 (implies --ai-provider custom)").addHelpText("after", AUDIT_HELP_TEXT).action(async (options) => {
4093
+ program.command("audit").description("Run an SEO + GEO audit (280+ checks)").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("--fail-on <level>", "CI gate: exit 1 if any issue is at or above this severity (error, warning, notice)").option("--min-score <n>", "CI gate: exit 1 if the overall score is below n (0-100)").option("--ai", "Enable AI analysis (your own key, or a local model)").option("--ai-provider <provider>", "AI provider (auto-detected if omitted; Apple Intelligence on a Mac that has it, or set RANKCLI_AI_PROVIDER): apple, openrouter, openai, anthropic, groq, gemini, deepseek, mistral, cerebras, together, fireworks, ollama, lmstudio, llamacpp, vllm, custom").option("--ai-key <key>", "AI provider API key (or set the provider's env var, e.g. OPENROUTER_API_KEY)").option("--ai-model <model>", "Model id (defaults to a cheap model per provider, or the local server's loaded model)").option("--ai-base-url <url>", "OpenAI-compatible base URL, e.g. http://localhost:8080/v1 (implies --ai-provider custom)").addHelpText("after", AUDIT_HELP_TEXT).action(async (options) => {
3924
4094
  process.exitCode = await audit(options);
3925
4095
  });
3926
4096
  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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankcli/cli",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "description": "RankCLI - Ship code, get ranked. SEO meets CI/CD.",
5
5
  "homepage": "https://rankcli.dev",
6
6
  "main": "dist/index.js",
@@ -45,7 +45,7 @@
45
45
  "update-version": "node scripts/update-version.js"
46
46
  },
47
47
  "dependencies": {
48
- "@rankcli/agent-runtime": "^0.0.21",
48
+ "@rankcli/agent-runtime": "^0.0.23",
49
49
  "@supabase/supabase-js": "^2.98.0",
50
50
  "chalk": "^5.3.0",
51
51
  "commander": "^12.0.0",