@rankcli/cli 0.0.31 → 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 +5 -1
- package/dist/index.js +228 -8
- package/dist/index.mjs +234 -14
- 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
|
|----------|---------|----------|---------------|
|
package/dist/index.js
CHANGED
|
@@ -1453,6 +1453,177 @@ var TOGETHER_DEFAULT = "openai/gpt-oss-120b";
|
|
|
1453
1453
|
var FIREWORKS_DEFAULT = "accounts/fireworks/models/gpt-oss-120b";
|
|
1454
1454
|
var CEREBRAS_DEFAULT = "gpt-oss-120b";
|
|
1455
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
|
+
|
|
1456
1627
|
// src/lib/ai.ts
|
|
1457
1628
|
var AI_PROVIDERS = {
|
|
1458
1629
|
// Hosted
|
|
@@ -1467,6 +1638,7 @@ var AI_PROVIDERS = {
|
|
|
1467
1638
|
together: { label: "Together", baseUrl: "https://api.together.xyz/v1", model: TOGETHER_DEFAULT, envKeys: ["TOGETHER_API_KEY"] },
|
|
1468
1639
|
fireworks: { label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", model: FIREWORKS_DEFAULT, envKeys: ["FIREWORKS_API_KEY"] },
|
|
1469
1640
|
// Local / self-hosted
|
|
1641
|
+
apple: { label: "Apple Intelligence", model: "on-device", envKeys: [], local: true },
|
|
1470
1642
|
ollama: { label: "Ollama", baseUrl: "http://localhost:11434/v1", envKeys: [], local: true },
|
|
1471
1643
|
lmstudio: { label: "LM Studio", baseUrl: "http://localhost:1234/v1", envKeys: [], local: true },
|
|
1472
1644
|
llamacpp: { label: "llama.cpp", baseUrl: "http://localhost:8080/v1", envKeys: [], local: true },
|
|
@@ -1531,28 +1703,62 @@ async function firstServerModel(baseUrl, apiKey) {
|
|
|
1531
1703
|
}
|
|
1532
1704
|
function noProviderHints() {
|
|
1533
1705
|
return [
|
|
1706
|
+
"On a Mac with Apple Intelligence (macOS 26+), the on-device model is used automatically",
|
|
1534
1707
|
`Set one of ${AI_ENV_KEYS.join(", ")}, or pass --ai-key`,
|
|
1535
1708
|
"Or run a local model: start Ollama (https://ollama.com) and `ollama pull llama3.2` - it is picked up automatically",
|
|
1536
1709
|
"Or point at any OpenAI-compatible server: --ai-base-url http://localhost:8080/v1 --ai-model <name>"
|
|
1537
1710
|
];
|
|
1538
1711
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
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();
|
|
1541
1732
|
let notice;
|
|
1542
1733
|
let ollamaModels = null;
|
|
1543
1734
|
if (provider && !AI_PROVIDERS[provider]) {
|
|
1544
1735
|
return {
|
|
1545
1736
|
ok: false,
|
|
1546
|
-
error: `Unknown AI provider "${
|
|
1737
|
+
error: `Unknown AI provider "${requested}"`,
|
|
1547
1738
|
hints: [`Choose one of: ${AI_PROVIDER_IDS.join(", ")}`]
|
|
1548
1739
|
};
|
|
1549
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
|
+
}
|
|
1550
1752
|
if (!provider) {
|
|
1551
1753
|
if (opts.aiBaseUrl) {
|
|
1552
1754
|
provider = "custom";
|
|
1553
1755
|
} else if (opts.aiKey) {
|
|
1554
1756
|
provider = providerFromKey(opts.aiKey) ?? "openai";
|
|
1555
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
|
+
}
|
|
1556
1762
|
provider = DETECT_ORDER.find((id) => AI_PROVIDERS[id].envKeys.some((k) => env[k]));
|
|
1557
1763
|
}
|
|
1558
1764
|
}
|
|
@@ -1692,9 +1898,11 @@ function normalizeAnalysis(raw) {
|
|
|
1692
1898
|
}
|
|
1693
1899
|
var SEVERITY_ORDER = { critical: 0, error: 1, warning: 2, notice: 3, info: 3 };
|
|
1694
1900
|
var MAX_ISSUES_IN_PROMPT = 60;
|
|
1695
|
-
|
|
1901
|
+
var APPLE_MAX_ISSUES = 25;
|
|
1902
|
+
var APPLE_MAX_TOKENS = 1200;
|
|
1903
|
+
function buildPrompt(url, issues, maxIssues = MAX_ISSUES_IN_PROMPT) {
|
|
1696
1904
|
const ranked = [...issues].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4));
|
|
1697
|
-
const shown = ranked.slice(0,
|
|
1905
|
+
const shown = ranked.slice(0, maxIssues);
|
|
1698
1906
|
const more = ranked.length > shown.length ? `
|
|
1699
1907
|
(${ranked.length - shown.length} lower-severity issues omitted)` : "";
|
|
1700
1908
|
return `Analyze these SEO audit results and provide prioritized recommendations:
|
|
@@ -1714,8 +1922,17 @@ Respond with only this JSON object - no prose, no code fences:
|
|
|
1714
1922
|
}`;
|
|
1715
1923
|
}
|
|
1716
1924
|
async function runAIAnalysis(url, issues, ai) {
|
|
1717
|
-
const prompt = buildPrompt(url, issues);
|
|
1718
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);
|
|
1719
1936
|
const timeoutMs = ai.local ? 3e5 : 9e4;
|
|
1720
1937
|
const maxTokens = 4096;
|
|
1721
1938
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -1765,6 +1982,9 @@ async function runAIAnalysis(url, issues, ai) {
|
|
|
1765
1982
|
if (!content) {
|
|
1766
1983
|
return { error: `${ai.label} returned no content (a reasoning model may have used its whole token budget - try another --ai-model)` };
|
|
1767
1984
|
}
|
|
1985
|
+
return parseAnalysis(content, ai);
|
|
1986
|
+
}
|
|
1987
|
+
function parseAnalysis(content, ai) {
|
|
1768
1988
|
const parsed = extractJsonObject(content);
|
|
1769
1989
|
const analysis = parsed ? normalizeAnalysis(parsed) : null;
|
|
1770
1990
|
if (!analysis) {
|
|
@@ -4002,7 +4222,7 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
4002
4222
|
}
|
|
4003
4223
|
|
|
4004
4224
|
// src/version.ts
|
|
4005
|
-
var VERSION = "0.0.
|
|
4225
|
+
var VERSION = "0.0.32";
|
|
4006
4226
|
|
|
4007
4227
|
// src/index.ts
|
|
4008
4228
|
var program = new import_commander.Command();
|
|
@@ -4014,7 +4234,7 @@ program.command("logout").description("Log out of your RankCLI account").action(
|
|
|
4014
4234
|
program.command("whoami").description("Show current logged in user").action(whoami);
|
|
4015
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);
|
|
4016
4236
|
program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
|
|
4017
|
-
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) => {
|
|
4018
4238
|
process.exitCode = await audit(options);
|
|
4019
4239
|
});
|
|
4020
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
|
@@ -1274,6 +1274,177 @@ var TOGETHER_DEFAULT = "openai/gpt-oss-120b";
|
|
|
1274
1274
|
var FIREWORKS_DEFAULT = "accounts/fireworks/models/gpt-oss-120b";
|
|
1275
1275
|
var CEREBRAS_DEFAULT = "gpt-oss-120b";
|
|
1276
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
|
+
|
|
1277
1448
|
// src/lib/ai.ts
|
|
1278
1449
|
var AI_PROVIDERS = {
|
|
1279
1450
|
// Hosted
|
|
@@ -1288,6 +1459,7 @@ var AI_PROVIDERS = {
|
|
|
1288
1459
|
together: { label: "Together", baseUrl: "https://api.together.xyz/v1", model: TOGETHER_DEFAULT, envKeys: ["TOGETHER_API_KEY"] },
|
|
1289
1460
|
fireworks: { label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", model: FIREWORKS_DEFAULT, envKeys: ["FIREWORKS_API_KEY"] },
|
|
1290
1461
|
// Local / self-hosted
|
|
1462
|
+
apple: { label: "Apple Intelligence", model: "on-device", envKeys: [], local: true },
|
|
1291
1463
|
ollama: { label: "Ollama", baseUrl: "http://localhost:11434/v1", envKeys: [], local: true },
|
|
1292
1464
|
lmstudio: { label: "LM Studio", baseUrl: "http://localhost:1234/v1", envKeys: [], local: true },
|
|
1293
1465
|
llamacpp: { label: "llama.cpp", baseUrl: "http://localhost:8080/v1", envKeys: [], local: true },
|
|
@@ -1352,28 +1524,62 @@ async function firstServerModel(baseUrl, apiKey) {
|
|
|
1352
1524
|
}
|
|
1353
1525
|
function noProviderHints() {
|
|
1354
1526
|
return [
|
|
1527
|
+
"On a Mac with Apple Intelligence (macOS 26+), the on-device model is used automatically",
|
|
1355
1528
|
`Set one of ${AI_ENV_KEYS.join(", ")}, or pass --ai-key`,
|
|
1356
1529
|
"Or run a local model: start Ollama (https://ollama.com) and `ollama pull llama3.2` - it is picked up automatically",
|
|
1357
1530
|
"Or point at any OpenAI-compatible server: --ai-base-url http://localhost:8080/v1 --ai-model <name>"
|
|
1358
1531
|
];
|
|
1359
1532
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
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();
|
|
1362
1553
|
let notice;
|
|
1363
1554
|
let ollamaModels = null;
|
|
1364
1555
|
if (provider && !AI_PROVIDERS[provider]) {
|
|
1365
1556
|
return {
|
|
1366
1557
|
ok: false,
|
|
1367
|
-
error: `Unknown AI provider "${
|
|
1558
|
+
error: `Unknown AI provider "${requested}"`,
|
|
1368
1559
|
hints: [`Choose one of: ${AI_PROVIDER_IDS.join(", ")}`]
|
|
1369
1560
|
};
|
|
1370
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
|
+
}
|
|
1371
1573
|
if (!provider) {
|
|
1372
1574
|
if (opts.aiBaseUrl) {
|
|
1373
1575
|
provider = "custom";
|
|
1374
1576
|
} else if (opts.aiKey) {
|
|
1375
1577
|
provider = providerFromKey(opts.aiKey) ?? "openai";
|
|
1376
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
|
+
}
|
|
1377
1583
|
provider = DETECT_ORDER.find((id) => AI_PROVIDERS[id].envKeys.some((k) => env[k]));
|
|
1378
1584
|
}
|
|
1379
1585
|
}
|
|
@@ -1513,9 +1719,11 @@ function normalizeAnalysis(raw) {
|
|
|
1513
1719
|
}
|
|
1514
1720
|
var SEVERITY_ORDER = { critical: 0, error: 1, warning: 2, notice: 3, info: 3 };
|
|
1515
1721
|
var MAX_ISSUES_IN_PROMPT = 60;
|
|
1516
|
-
|
|
1722
|
+
var APPLE_MAX_ISSUES = 25;
|
|
1723
|
+
var APPLE_MAX_TOKENS = 1200;
|
|
1724
|
+
function buildPrompt(url, issues, maxIssues = MAX_ISSUES_IN_PROMPT) {
|
|
1517
1725
|
const ranked = [...issues].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4));
|
|
1518
|
-
const shown = ranked.slice(0,
|
|
1726
|
+
const shown = ranked.slice(0, maxIssues);
|
|
1519
1727
|
const more = ranked.length > shown.length ? `
|
|
1520
1728
|
(${ranked.length - shown.length} lower-severity issues omitted)` : "";
|
|
1521
1729
|
return `Analyze these SEO audit results and provide prioritized recommendations:
|
|
@@ -1535,8 +1743,17 @@ Respond with only this JSON object - no prose, no code fences:
|
|
|
1535
1743
|
}`;
|
|
1536
1744
|
}
|
|
1537
1745
|
async function runAIAnalysis(url, issues, ai) {
|
|
1538
|
-
const prompt = buildPrompt(url, issues);
|
|
1539
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);
|
|
1540
1757
|
const timeoutMs = ai.local ? 3e5 : 9e4;
|
|
1541
1758
|
const maxTokens = 4096;
|
|
1542
1759
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -1586,6 +1803,9 @@ async function runAIAnalysis(url, issues, ai) {
|
|
|
1586
1803
|
if (!content) {
|
|
1587
1804
|
return { error: `${ai.label} returned no content (a reasoning model may have used its whole token budget - try another --ai-model)` };
|
|
1588
1805
|
}
|
|
1806
|
+
return parseAnalysis(content, ai);
|
|
1807
|
+
}
|
|
1808
|
+
function parseAnalysis(content, ai) {
|
|
1589
1809
|
const parsed = extractJsonObject(content);
|
|
1590
1810
|
const analysis = parsed ? normalizeAnalysis(parsed) : null;
|
|
1591
1811
|
if (!analysis) {
|
|
@@ -2059,8 +2279,8 @@ Examples:
|
|
|
2059
2279
|
`;
|
|
2060
2280
|
|
|
2061
2281
|
// src/commands/apply.ts
|
|
2062
|
-
import { existsSync as
|
|
2063
|
-
import { join as
|
|
2282
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2283
|
+
import { join as join5 } from "path";
|
|
2064
2284
|
import {
|
|
2065
2285
|
runAuditWithFixes,
|
|
2066
2286
|
generateFixes,
|
|
@@ -2094,11 +2314,11 @@ function truncate(str, maxLen) {
|
|
|
2094
2314
|
}
|
|
2095
2315
|
async function apply(options) {
|
|
2096
2316
|
const cwd = process.cwd();
|
|
2097
|
-
const configPath =
|
|
2317
|
+
const configPath = join5(cwd, "rankcli.config.json");
|
|
2098
2318
|
let config2 = null;
|
|
2099
|
-
if (
|
|
2319
|
+
if (existsSync5(configPath)) {
|
|
2100
2320
|
try {
|
|
2101
|
-
config2 = JSON.parse(
|
|
2321
|
+
config2 = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
2102
2322
|
} catch {
|
|
2103
2323
|
}
|
|
2104
2324
|
}
|
|
@@ -2222,7 +2442,7 @@ async function apply(options) {
|
|
|
2222
2442
|
}
|
|
2223
2443
|
if (config2 && !config2.url) {
|
|
2224
2444
|
config2.url = url;
|
|
2225
|
-
|
|
2445
|
+
writeFileSync4(configPath, JSON.stringify(config2, null, 2));
|
|
2226
2446
|
console.log("\n\u{1F4BE} Updated config with URL.");
|
|
2227
2447
|
}
|
|
2228
2448
|
console.log("\n" + "\u2500".repeat(50));
|
|
@@ -3858,7 +4078,7 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
3858
4078
|
}
|
|
3859
4079
|
|
|
3860
4080
|
// src/version.ts
|
|
3861
|
-
var VERSION = "0.0.
|
|
4081
|
+
var VERSION = "0.0.32";
|
|
3862
4082
|
|
|
3863
4083
|
// src/index.ts
|
|
3864
4084
|
var program = new Command();
|
|
@@ -3870,7 +4090,7 @@ program.command("logout").description("Log out of your RankCLI account").action(
|
|
|
3870
4090
|
program.command("whoami").description("Show current logged in user").action(whoami);
|
|
3871
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);
|
|
3872
4092
|
program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
|
|
3873
|
-
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) => {
|
|
3874
4094
|
process.exitCode = await audit(options);
|
|
3875
4095
|
});
|
|
3876
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",
|