@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 +14 -8
- package/dist/index.js +246 -76
- package/dist/index.mjs +252 -82
- package/package.json +2 -2
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.
|
|
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:
|
|
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
|
|
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
|
|
282
|
-
|
|
|
283
|
-
|
|
|
284
|
-
|
|
|
285
|
-
|
|
|
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 ||
|
|
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
|
|
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 ||
|
|
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
|
|
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:
|
|
1116
|
+
checks: 280,
|
|
1115
1117
|
sites: 0,
|
|
1116
1118
|
domains_per_month: 1,
|
|
1117
|
-
features: { geo:
|
|
1119
|
+
features: { geo: true, auto_fix: true, github: false, ai: false }
|
|
1118
1120
|
},
|
|
1119
1121
|
free: {
|
|
1120
|
-
checks:
|
|
1122
|
+
checks: 280,
|
|
1121
1123
|
sites: 1,
|
|
1122
1124
|
domains_per_month: 2,
|
|
1123
|
-
features: { geo:
|
|
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:
|
|
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}
|
|
1156
|
-
import_chalk3.default.dim("
|
|
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
|
-
|
|
1561
|
-
|
|
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 "${
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
3996
|
-
console.log("2. Add alert config at https://rankcli.dev/account
|
|
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.
|
|
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
|
|
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 ||
|
|
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
|
|
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 ||
|
|
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
|
|
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:
|
|
1103
|
+
checks: 280,
|
|
1102
1104
|
sites: 0,
|
|
1103
1105
|
domains_per_month: 1,
|
|
1104
|
-
features: { geo:
|
|
1106
|
+
features: { geo: true, auto_fix: true, github: false, ai: false }
|
|
1105
1107
|
},
|
|
1106
1108
|
free: {
|
|
1107
|
-
checks:
|
|
1109
|
+
checks: 280,
|
|
1108
1110
|
sites: 1,
|
|
1109
1111
|
domains_per_month: 2,
|
|
1110
|
-
features: { geo:
|
|
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:
|
|
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}
|
|
1143
|
-
chalk3.dim("
|
|
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
|
-
|
|
1382
|
-
|
|
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 "${
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
2095
|
-
import { join as
|
|
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 =
|
|
2317
|
+
const configPath = join5(cwd, "rankcli.config.json");
|
|
2130
2318
|
let config2 = null;
|
|
2131
|
-
if (
|
|
2319
|
+
if (existsSync5(configPath)) {
|
|
2132
2320
|
try {
|
|
2133
|
-
config2 = JSON.parse(
|
|
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
|
-
|
|
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
|
|
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
|
|
3852
|
-
console.log("2. Add alert config at https://rankcli.dev/account
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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",
|