@rankcli/cli 0.0.32 → 0.0.33
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 +1 -1
- package/dist/index.js +85 -66
- package/dist/index.mjs +85 -66
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ rankcli apply --url https://mysite.com --auto # Auto-apply
|
|
|
211
211
|
|
|
212
212
|
### `rankcli keywords`
|
|
213
213
|
|
|
214
|
-
AI-powered keyword research.
|
|
214
|
+
AI-powered keyword research. `--ai` runs on the same provider as `audit --ai` (Apple Intelligence on a Mac that has it, else your key, else a local Ollama) and takes the same `--ai-provider` / `--ai-key` / `--ai-model` / `--ai-base-url` flags. Keyword clustering uses embeddings on OpenAI and term vectors everywhere else; local models judge 5 tool ideas instead of 15.
|
|
215
215
|
|
|
216
216
|
```bash
|
|
217
217
|
rankcli keywords --url https://mysite.com --ai
|
package/dist/index.js
CHANGED
|
@@ -1922,19 +1922,27 @@ Respond with only this JSON object - no prose, no code fences:
|
|
|
1922
1922
|
}`;
|
|
1923
1923
|
}
|
|
1924
1924
|
async function runAIAnalysis(url, issues, ai) {
|
|
1925
|
-
const
|
|
1925
|
+
const reply = await completeWithAI(ai, {
|
|
1926
|
+
system: "You are an SEO expert. Respond with valid JSON only.",
|
|
1927
|
+
prompt: buildPrompt(url, issues, ai.provider === "apple" ? APPLE_MAX_ISSUES : MAX_ISSUES_IN_PROMPT),
|
|
1928
|
+
json: true
|
|
1929
|
+
});
|
|
1930
|
+
if ("error" in reply) return reply;
|
|
1931
|
+
return parseAnalysis(reply.text, ai);
|
|
1932
|
+
}
|
|
1933
|
+
async function completeWithAI(ai, request) {
|
|
1934
|
+
const { system, prompt } = request;
|
|
1926
1935
|
if (ai.provider === "apple" && ai.helper) {
|
|
1927
1936
|
const reply = await generateWithAppleAI(ai.helper, {
|
|
1928
1937
|
system,
|
|
1929
|
-
prompt
|
|
1930
|
-
maxTokens: APPLE_MAX_TOKENS
|
|
1938
|
+
prompt,
|
|
1939
|
+
maxTokens: Math.min(request.maxTokens ?? APPLE_MAX_TOKENS, APPLE_MAX_TOKENS)
|
|
1931
1940
|
});
|
|
1932
1941
|
if ("error" in reply) return { error: `${ai.label} request failed: ${reply.error}` };
|
|
1933
|
-
return
|
|
1942
|
+
return reply;
|
|
1934
1943
|
}
|
|
1935
|
-
const prompt = buildPrompt(url, issues);
|
|
1936
1944
|
const timeoutMs = ai.local ? 3e5 : 9e4;
|
|
1937
|
-
const maxTokens = 4096;
|
|
1945
|
+
const maxTokens = Math.max(request.maxTokens ?? 4096, 4096);
|
|
1938
1946
|
const headers = { "Content-Type": "application/json" };
|
|
1939
1947
|
let endpoint;
|
|
1940
1948
|
let body;
|
|
@@ -1942,7 +1950,7 @@ async function runAIAnalysis(url, issues, ai) {
|
|
|
1942
1950
|
endpoint = `${ai.baseUrl}/messages`;
|
|
1943
1951
|
headers["x-api-key"] = ai.apiKey || "";
|
|
1944
1952
|
headers["anthropic-version"] = "2023-06-01";
|
|
1945
|
-
body = { model: ai.model, max_tokens: maxTokens, system, messages: [{ role: "user", content: prompt }] };
|
|
1953
|
+
body = { model: ai.model, max_tokens: maxTokens, ...system ? { system } : {}, messages: [{ role: "user", content: prompt }] };
|
|
1946
1954
|
} else {
|
|
1947
1955
|
endpoint = `${ai.baseUrl}/chat/completions`;
|
|
1948
1956
|
if (ai.apiKey) headers["Authorization"] = `Bearer ${ai.apiKey}`;
|
|
@@ -1953,12 +1961,12 @@ async function runAIAnalysis(url, issues, ai) {
|
|
|
1953
1961
|
body = {
|
|
1954
1962
|
model: ai.model,
|
|
1955
1963
|
messages: [
|
|
1956
|
-
{ role: "system", content: system },
|
|
1964
|
+
...system ? [{ role: "system", content: system }] : [],
|
|
1957
1965
|
{ role: "user", content: prompt }
|
|
1958
1966
|
],
|
|
1959
1967
|
// OpenAI's GPT-5.6 rejects max_tokens (and temperature); everyone else expects max_tokens.
|
|
1960
1968
|
...ai.provider === "openai" && !ai.local ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
|
|
1961
|
-
...ai.jsonMode ? { response_format: { type: "json_object" } } : {}
|
|
1969
|
+
...ai.jsonMode && request.json ? { response_format: { type: "json_object" } } : {}
|
|
1962
1970
|
};
|
|
1963
1971
|
}
|
|
1964
1972
|
let response;
|
|
@@ -1982,7 +1990,7 @@ async function runAIAnalysis(url, issues, ai) {
|
|
|
1982
1990
|
if (!content) {
|
|
1983
1991
|
return { error: `${ai.label} returned no content (a reasoning model may have used its whole token budget - try another --ai-model)` };
|
|
1984
1992
|
}
|
|
1985
|
-
return
|
|
1993
|
+
return { text: content };
|
|
1986
1994
|
}
|
|
1987
1995
|
function parseAnalysis(content, ai) {
|
|
1988
1996
|
const parsed = extractJsonObject(content);
|
|
@@ -3238,7 +3246,7 @@ async function trackHistory(options) {
|
|
|
3238
3246
|
}
|
|
3239
3247
|
const days = options.days || 30;
|
|
3240
3248
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
3241
|
-
const { data: historyData, error } = await supabase.from("
|
|
3249
|
+
const { data: historyData, error } = await supabase.from("keyword_position_history").select("position, url, checked_at").eq("keyword_id", keywordData.id).gte("checked_at", cutoff.toISOString()).order("checked_at", { ascending: false });
|
|
3242
3250
|
if (error) {
|
|
3243
3251
|
spinner.fail(`Failed to load history: ${error.message}`);
|
|
3244
3252
|
return { success: false, error: error.message };
|
|
@@ -3247,7 +3255,7 @@ async function trackHistory(options) {
|
|
|
3247
3255
|
const history = (historyData || []).map((row) => ({
|
|
3248
3256
|
position: row.position,
|
|
3249
3257
|
url: row.url,
|
|
3250
|
-
recordedAt: new Date(row.
|
|
3258
|
+
recordedAt: new Date(row.checked_at)
|
|
3251
3259
|
}));
|
|
3252
3260
|
if (options.output === "json") {
|
|
3253
3261
|
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
@@ -3255,7 +3263,9 @@ async function trackHistory(options) {
|
|
|
3255
3263
|
console.log("\n" + import_chalk6.default.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
3256
3264
|
console.log("\u2550".repeat(50));
|
|
3257
3265
|
if (history.length === 0) {
|
|
3258
|
-
console.log(import_chalk6.default.dim(
|
|
3266
|
+
console.log(import_chalk6.default.dim(`
|
|
3267
|
+
No checks recorded yet. Run 'rankcli track check --url <your site>' to record one.
|
|
3268
|
+
`));
|
|
3259
3269
|
} else {
|
|
3260
3270
|
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
3261
3271
|
const sparkline = positions.map((p) => {
|
|
@@ -3266,7 +3276,7 @@ async function trackHistory(options) {
|
|
|
3266
3276
|
}).join("");
|
|
3267
3277
|
console.log(`
|
|
3268
3278
|
${sparkline} (last ${positions.length} checks)`);
|
|
3269
|
-
console.log(import_chalk6.default.dim(" \u2190 newer
|
|
3279
|
+
console.log(import_chalk6.default.dim(" \u2190 older newer \u2192\n"));
|
|
3270
3280
|
const recent = history.slice(0, 10);
|
|
3271
3281
|
for (const entry of recent) {
|
|
3272
3282
|
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
@@ -3580,6 +3590,40 @@ async function geo(options) {
|
|
|
3580
3590
|
// src/commands/keywords.ts
|
|
3581
3591
|
var readline2 = __toESM(require("readline"));
|
|
3582
3592
|
var import_agent_runtime7 = require("@rankcli/agent-runtime");
|
|
3593
|
+
|
|
3594
|
+
// src/lib/keyword-llm.ts
|
|
3595
|
+
async function openAIEmbeddings(ai, texts) {
|
|
3596
|
+
const response = await fetch(`${ai.baseUrl}/embeddings`, {
|
|
3597
|
+
method: "POST",
|
|
3598
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ai.apiKey}` },
|
|
3599
|
+
body: JSON.stringify({ model: "text-embedding-3-small", input: texts }),
|
|
3600
|
+
signal: AbortSignal.timeout(6e4)
|
|
3601
|
+
});
|
|
3602
|
+
if (!response.ok) throw new Error(`OpenAI embeddings returned HTTP ${response.status}`);
|
|
3603
|
+
const data = await response.json();
|
|
3604
|
+
return data.data.map((item) => item.embedding);
|
|
3605
|
+
}
|
|
3606
|
+
function keywordLLMFromAI(ai) {
|
|
3607
|
+
const llm = {
|
|
3608
|
+
async complete(prompt, opts = {}) {
|
|
3609
|
+
const reply = await completeWithAI(ai, { system: opts.system, prompt, maxTokens: opts.maxTokens, json: true });
|
|
3610
|
+
if ("error" in reply) throw new Error(reply.error);
|
|
3611
|
+
return reply.text;
|
|
3612
|
+
}
|
|
3613
|
+
};
|
|
3614
|
+
if (ai.provider === "openai" && ai.apiKey && ai.jsonMode) {
|
|
3615
|
+
llm.embed = (texts) => openAIEmbeddings(ai, texts);
|
|
3616
|
+
}
|
|
3617
|
+
if (ai.provider === "apple") {
|
|
3618
|
+
llm.maxContentChars = 5e3;
|
|
3619
|
+
llm.maxToolIdeas = 5;
|
|
3620
|
+
} else if (ai.local) {
|
|
3621
|
+
llm.maxToolIdeas = 5;
|
|
3622
|
+
}
|
|
3623
|
+
return llm;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// src/commands/keywords.ts
|
|
3583
3627
|
async function keywords(options) {
|
|
3584
3628
|
console.log("\n\u{1F511} RankCLI - Keyword Research\n");
|
|
3585
3629
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
@@ -3740,65 +3784,37 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
3740
3784
|
`);
|
|
3741
3785
|
}
|
|
3742
3786
|
}
|
|
3743
|
-
var SUPABASE_URL = "https://eqzlmjbvrtrglknphdai.supabase.co";
|
|
3744
3787
|
async function keywordsAI(url, options = {}) {
|
|
3745
3788
|
console.log("\n\u{1F916} AI-Powered Keyword Research\n");
|
|
3746
|
-
console.log("This uses GPT-4 to analyze your site and generate keyword recommendations");
|
|
3747
|
-
console.log("with competitive analysis and free tool ideas.\n");
|
|
3748
|
-
const openaiApiKey = options.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
3749
3789
|
try {
|
|
3750
3790
|
const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
|
|
3751
3791
|
url = parsed.href;
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
console.log(
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
headers: {
|
|
3760
|
-
"Content-Type": "application/json"
|
|
3761
|
-
// Auth header would go here if user is logged in
|
|
3762
|
-
},
|
|
3763
|
-
body: JSON.stringify({
|
|
3764
|
-
url,
|
|
3765
|
-
mode: "analyze"
|
|
3766
|
-
})
|
|
3767
|
-
});
|
|
3768
|
-
if (response.ok) {
|
|
3769
|
-
const result2 = await response.json();
|
|
3770
|
-
if (result2.tier === "free" && result2.teaser) {
|
|
3771
|
-
console.log("\n" + "\u2500".repeat(60));
|
|
3772
|
-
console.log("\u{1F512} FREE TIER - Limited Preview");
|
|
3773
|
-
console.log("\u2500".repeat(60));
|
|
3774
|
-
console.log(`Found ${result2.teaser.totalKeywordsFound} keywords`);
|
|
3775
|
-
console.log(`Top keywords: ${result2.teaser.topKeywords.join(", ")}`);
|
|
3776
|
-
console.log(`
|
|
3777
|
-
${result2.teaser.upgradeMessage}`);
|
|
3778
|
-
console.log('\n\u{1F4A1} Run "rankcli login" to access your paid plan features.\n');
|
|
3779
|
-
return;
|
|
3780
|
-
}
|
|
3781
|
-
displayAIKeywordResults(result2);
|
|
3782
|
-
return;
|
|
3783
|
-
} else {
|
|
3784
|
-
const errorText = await response.text();
|
|
3785
|
-
console.log(`\u26A0\uFE0F Cloud request failed, running locally...
|
|
3786
|
-
`);
|
|
3787
|
-
}
|
|
3788
|
-
} catch (err) {
|
|
3789
|
-
console.log("\u26A0\uFE0F Cloud unavailable, running locally...\n");
|
|
3790
|
-
}
|
|
3792
|
+
const resolution = await resolveAIProvider(options);
|
|
3793
|
+
if (!resolution.ok) {
|
|
3794
|
+
console.log(`\u274C ${resolution.error}`);
|
|
3795
|
+
for (const hint of resolution.hints) console.log(` ${hint}`);
|
|
3796
|
+
console.log("");
|
|
3797
|
+
process.exitCode = 1;
|
|
3798
|
+
return;
|
|
3791
3799
|
}
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3800
|
+
const { ai } = resolution;
|
|
3801
|
+
if (ai.notice) console.log(ai.notice);
|
|
3802
|
+
const check = await completeWithAI(ai, { prompt: 'Reply with the JSON object {"ok": true} and nothing else.', maxTokens: 50, json: true });
|
|
3803
|
+
if ("error" in check) {
|
|
3804
|
+
console.log(`\u274C ${check.error}`);
|
|
3805
|
+
console.log(" Choose another provider with --ai-provider, or fix the key or model and try again.\n");
|
|
3806
|
+
process.exitCode = 1;
|
|
3795
3807
|
return;
|
|
3796
3808
|
}
|
|
3797
|
-
console.log(
|
|
3809
|
+
console.log(`\u{1F4CD} Analyzing: ${url}`);
|
|
3810
|
+
console.log(`\u{1F9E0} Model: ${ai.label} (${ai.model})
|
|
3811
|
+
`);
|
|
3812
|
+
console.log(`\u{1F52C} Running AI analysis (${ai.local ? "a few minutes on a local model" : "this may take a minute"})...
|
|
3813
|
+
`);
|
|
3798
3814
|
const result = await (0, import_agent_runtime7.runAIKeywordResearch)({
|
|
3799
3815
|
url,
|
|
3800
3816
|
tier: "solo",
|
|
3801
|
-
|
|
3817
|
+
llm: keywordLLMFromAI(ai),
|
|
3802
3818
|
enableEnhancedToolIdeas: true,
|
|
3803
3819
|
competitiveSearchKeys: {
|
|
3804
3820
|
braveApiKey: process.env.BRAVE_API_KEY,
|
|
@@ -3811,6 +3827,7 @@ ${result2.teaser.upgradeMessage}`);
|
|
|
3811
3827
|
console.log(`
|
|
3812
3828
|
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
3813
3829
|
`);
|
|
3830
|
+
process.exitCode = 1;
|
|
3814
3831
|
}
|
|
3815
3832
|
}
|
|
3816
3833
|
function displayAIKeywordResults(result) {
|
|
@@ -4222,7 +4239,7 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
4222
4239
|
}
|
|
4223
4240
|
|
|
4224
4241
|
// src/version.ts
|
|
4225
|
-
var VERSION = "0.0.
|
|
4242
|
+
var VERSION = "0.0.33";
|
|
4226
4243
|
|
|
4227
4244
|
// src/index.ts
|
|
4228
4245
|
var program = new import_commander.Command();
|
|
@@ -4253,11 +4270,13 @@ program.command("fix").description("Agentic SEO fixer - detect, fix, commit, and
|
|
|
4253
4270
|
}
|
|
4254
4271
|
});
|
|
4255
4272
|
program.command("geo").description("GEO analysis - Check AI search visibility (ChatGPT, Perplexity, Claude)").option("-u, --url <url>", "URL to analyze").option("-o, --output <format>", "Output format (json, console)", "console").action(geo);
|
|
4256
|
-
program.command("keywords").description("AI-powered keyword research with actionable suggestions").option("-u, --url <url>", "Your website URL").option("-s, --seed <keywords>", "Seed keywords (comma-separated)").option("--auto", "Auto-extract seed keywords from your page").option("--quick", "Quick mode - skip questions, use defaults").option("--ai", "
|
|
4273
|
+
program.command("keywords").description("AI-powered keyword research with actionable suggestions").option("-u, --url <url>", "Your website URL").option("-s, --seed <keywords>", "Seed keywords (comma-separated)").option("--auto", "Auto-extract seed keywords from your page").option("--quick", "Quick mode - skip questions, use defaults").option("--ai", "AI keyword research with free tool ideas, on your own provider (Apple Intelligence on a Mac that has it)").option("--ai-provider <provider>", "AI provider for --ai (auto-detected if omitted, or set RANKCLI_AI_PROVIDER)").option("--ai-key <key>", "AI provider API key (or the provider's env var)").option("--ai-model <model>", "Override the provider's default model").option("--ai-base-url <url>", "Any OpenAI-compatible server (implies --ai-provider custom)").option("--local", "No effect; --ai always runs locally on your provider (kept for old scripts)").option("--competitor", "Competitor gap analysis mode (SpyFu Kombat-style)").option("-c, --competitors <domains>", "Competitor domains (comma-separated)").action(async (options) => {
|
|
4257
4274
|
if (options.ai && options.url) {
|
|
4258
4275
|
await keywordsAI(options.url, {
|
|
4259
|
-
|
|
4260
|
-
|
|
4276
|
+
aiProvider: options.aiProvider,
|
|
4277
|
+
aiKey: options.aiKey,
|
|
4278
|
+
aiModel: options.aiModel,
|
|
4279
|
+
aiBaseUrl: options.aiBaseUrl
|
|
4261
4280
|
});
|
|
4262
4281
|
} else if (options.competitor && options.url && options.seed) {
|
|
4263
4282
|
const seeds = options.seed.split(",").map((s) => s.trim());
|
package/dist/index.mjs
CHANGED
|
@@ -1743,19 +1743,27 @@ Respond with only this JSON object - no prose, no code fences:
|
|
|
1743
1743
|
}`;
|
|
1744
1744
|
}
|
|
1745
1745
|
async function runAIAnalysis(url, issues, ai) {
|
|
1746
|
-
const
|
|
1746
|
+
const reply = await completeWithAI(ai, {
|
|
1747
|
+
system: "You are an SEO expert. Respond with valid JSON only.",
|
|
1748
|
+
prompt: buildPrompt(url, issues, ai.provider === "apple" ? APPLE_MAX_ISSUES : MAX_ISSUES_IN_PROMPT),
|
|
1749
|
+
json: true
|
|
1750
|
+
});
|
|
1751
|
+
if ("error" in reply) return reply;
|
|
1752
|
+
return parseAnalysis(reply.text, ai);
|
|
1753
|
+
}
|
|
1754
|
+
async function completeWithAI(ai, request) {
|
|
1755
|
+
const { system, prompt } = request;
|
|
1747
1756
|
if (ai.provider === "apple" && ai.helper) {
|
|
1748
1757
|
const reply = await generateWithAppleAI(ai.helper, {
|
|
1749
1758
|
system,
|
|
1750
|
-
prompt
|
|
1751
|
-
maxTokens: APPLE_MAX_TOKENS
|
|
1759
|
+
prompt,
|
|
1760
|
+
maxTokens: Math.min(request.maxTokens ?? APPLE_MAX_TOKENS, APPLE_MAX_TOKENS)
|
|
1752
1761
|
});
|
|
1753
1762
|
if ("error" in reply) return { error: `${ai.label} request failed: ${reply.error}` };
|
|
1754
|
-
return
|
|
1763
|
+
return reply;
|
|
1755
1764
|
}
|
|
1756
|
-
const prompt = buildPrompt(url, issues);
|
|
1757
1765
|
const timeoutMs = ai.local ? 3e5 : 9e4;
|
|
1758
|
-
const maxTokens = 4096;
|
|
1766
|
+
const maxTokens = Math.max(request.maxTokens ?? 4096, 4096);
|
|
1759
1767
|
const headers = { "Content-Type": "application/json" };
|
|
1760
1768
|
let endpoint;
|
|
1761
1769
|
let body;
|
|
@@ -1763,7 +1771,7 @@ async function runAIAnalysis(url, issues, ai) {
|
|
|
1763
1771
|
endpoint = `${ai.baseUrl}/messages`;
|
|
1764
1772
|
headers["x-api-key"] = ai.apiKey || "";
|
|
1765
1773
|
headers["anthropic-version"] = "2023-06-01";
|
|
1766
|
-
body = { model: ai.model, max_tokens: maxTokens, system, messages: [{ role: "user", content: prompt }] };
|
|
1774
|
+
body = { model: ai.model, max_tokens: maxTokens, ...system ? { system } : {}, messages: [{ role: "user", content: prompt }] };
|
|
1767
1775
|
} else {
|
|
1768
1776
|
endpoint = `${ai.baseUrl}/chat/completions`;
|
|
1769
1777
|
if (ai.apiKey) headers["Authorization"] = `Bearer ${ai.apiKey}`;
|
|
@@ -1774,12 +1782,12 @@ async function runAIAnalysis(url, issues, ai) {
|
|
|
1774
1782
|
body = {
|
|
1775
1783
|
model: ai.model,
|
|
1776
1784
|
messages: [
|
|
1777
|
-
{ role: "system", content: system },
|
|
1785
|
+
...system ? [{ role: "system", content: system }] : [],
|
|
1778
1786
|
{ role: "user", content: prompt }
|
|
1779
1787
|
],
|
|
1780
1788
|
// OpenAI's GPT-5.6 rejects max_tokens (and temperature); everyone else expects max_tokens.
|
|
1781
1789
|
...ai.provider === "openai" && !ai.local ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
|
|
1782
|
-
...ai.jsonMode ? { response_format: { type: "json_object" } } : {}
|
|
1790
|
+
...ai.jsonMode && request.json ? { response_format: { type: "json_object" } } : {}
|
|
1783
1791
|
};
|
|
1784
1792
|
}
|
|
1785
1793
|
let response;
|
|
@@ -1803,7 +1811,7 @@ async function runAIAnalysis(url, issues, ai) {
|
|
|
1803
1811
|
if (!content) {
|
|
1804
1812
|
return { error: `${ai.label} returned no content (a reasoning model may have used its whole token budget - try another --ai-model)` };
|
|
1805
1813
|
}
|
|
1806
|
-
return
|
|
1814
|
+
return { text: content };
|
|
1807
1815
|
}
|
|
1808
1816
|
function parseAnalysis(content, ai) {
|
|
1809
1817
|
const parsed = extractJsonObject(content);
|
|
@@ -3069,7 +3077,7 @@ async function trackHistory(options) {
|
|
|
3069
3077
|
}
|
|
3070
3078
|
const days = options.days || 30;
|
|
3071
3079
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
3072
|
-
const { data: historyData, error } = await supabase.from("
|
|
3080
|
+
const { data: historyData, error } = await supabase.from("keyword_position_history").select("position, url, checked_at").eq("keyword_id", keywordData.id).gte("checked_at", cutoff.toISOString()).order("checked_at", { ascending: false });
|
|
3073
3081
|
if (error) {
|
|
3074
3082
|
spinner.fail(`Failed to load history: ${error.message}`);
|
|
3075
3083
|
return { success: false, error: error.message };
|
|
@@ -3078,7 +3086,7 @@ async function trackHistory(options) {
|
|
|
3078
3086
|
const history = (historyData || []).map((row) => ({
|
|
3079
3087
|
position: row.position,
|
|
3080
3088
|
url: row.url,
|
|
3081
|
-
recordedAt: new Date(row.
|
|
3089
|
+
recordedAt: new Date(row.checked_at)
|
|
3082
3090
|
}));
|
|
3083
3091
|
if (options.output === "json") {
|
|
3084
3092
|
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
@@ -3086,7 +3094,9 @@ async function trackHistory(options) {
|
|
|
3086
3094
|
console.log("\n" + chalk6.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
3087
3095
|
console.log("\u2550".repeat(50));
|
|
3088
3096
|
if (history.length === 0) {
|
|
3089
|
-
console.log(chalk6.dim(
|
|
3097
|
+
console.log(chalk6.dim(`
|
|
3098
|
+
No checks recorded yet. Run 'rankcli track check --url <your site>' to record one.
|
|
3099
|
+
`));
|
|
3090
3100
|
} else {
|
|
3091
3101
|
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
3092
3102
|
const sparkline = positions.map((p) => {
|
|
@@ -3097,7 +3107,7 @@ async function trackHistory(options) {
|
|
|
3097
3107
|
}).join("");
|
|
3098
3108
|
console.log(`
|
|
3099
3109
|
${sparkline} (last ${positions.length} checks)`);
|
|
3100
|
-
console.log(chalk6.dim(" \u2190 newer
|
|
3110
|
+
console.log(chalk6.dim(" \u2190 older newer \u2192\n"));
|
|
3101
3111
|
const recent = history.slice(0, 10);
|
|
3102
3112
|
for (const entry of recent) {
|
|
3103
3113
|
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
@@ -3421,6 +3431,40 @@ import {
|
|
|
3421
3431
|
formatTopicReport,
|
|
3422
3432
|
runAIKeywordResearch
|
|
3423
3433
|
} from "@rankcli/agent-runtime";
|
|
3434
|
+
|
|
3435
|
+
// src/lib/keyword-llm.ts
|
|
3436
|
+
async function openAIEmbeddings(ai, texts) {
|
|
3437
|
+
const response = await fetch(`${ai.baseUrl}/embeddings`, {
|
|
3438
|
+
method: "POST",
|
|
3439
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ai.apiKey}` },
|
|
3440
|
+
body: JSON.stringify({ model: "text-embedding-3-small", input: texts }),
|
|
3441
|
+
signal: AbortSignal.timeout(6e4)
|
|
3442
|
+
});
|
|
3443
|
+
if (!response.ok) throw new Error(`OpenAI embeddings returned HTTP ${response.status}`);
|
|
3444
|
+
const data = await response.json();
|
|
3445
|
+
return data.data.map((item) => item.embedding);
|
|
3446
|
+
}
|
|
3447
|
+
function keywordLLMFromAI(ai) {
|
|
3448
|
+
const llm = {
|
|
3449
|
+
async complete(prompt, opts = {}) {
|
|
3450
|
+
const reply = await completeWithAI(ai, { system: opts.system, prompt, maxTokens: opts.maxTokens, json: true });
|
|
3451
|
+
if ("error" in reply) throw new Error(reply.error);
|
|
3452
|
+
return reply.text;
|
|
3453
|
+
}
|
|
3454
|
+
};
|
|
3455
|
+
if (ai.provider === "openai" && ai.apiKey && ai.jsonMode) {
|
|
3456
|
+
llm.embed = (texts) => openAIEmbeddings(ai, texts);
|
|
3457
|
+
}
|
|
3458
|
+
if (ai.provider === "apple") {
|
|
3459
|
+
llm.maxContentChars = 5e3;
|
|
3460
|
+
llm.maxToolIdeas = 5;
|
|
3461
|
+
} else if (ai.local) {
|
|
3462
|
+
llm.maxToolIdeas = 5;
|
|
3463
|
+
}
|
|
3464
|
+
return llm;
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
// src/commands/keywords.ts
|
|
3424
3468
|
async function keywords(options) {
|
|
3425
3469
|
console.log("\n\u{1F511} RankCLI - Keyword Research\n");
|
|
3426
3470
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
@@ -3581,65 +3625,37 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
3581
3625
|
`);
|
|
3582
3626
|
}
|
|
3583
3627
|
}
|
|
3584
|
-
var SUPABASE_URL = "https://eqzlmjbvrtrglknphdai.supabase.co";
|
|
3585
3628
|
async function keywordsAI(url, options = {}) {
|
|
3586
3629
|
console.log("\n\u{1F916} AI-Powered Keyword Research\n");
|
|
3587
|
-
console.log("This uses GPT-4 to analyze your site and generate keyword recommendations");
|
|
3588
|
-
console.log("with competitive analysis and free tool ideas.\n");
|
|
3589
|
-
const openaiApiKey = options.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
3590
3630
|
try {
|
|
3591
3631
|
const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
|
|
3592
3632
|
url = parsed.href;
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
console.log(
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
headers: {
|
|
3601
|
-
"Content-Type": "application/json"
|
|
3602
|
-
// Auth header would go here if user is logged in
|
|
3603
|
-
},
|
|
3604
|
-
body: JSON.stringify({
|
|
3605
|
-
url,
|
|
3606
|
-
mode: "analyze"
|
|
3607
|
-
})
|
|
3608
|
-
});
|
|
3609
|
-
if (response.ok) {
|
|
3610
|
-
const result2 = await response.json();
|
|
3611
|
-
if (result2.tier === "free" && result2.teaser) {
|
|
3612
|
-
console.log("\n" + "\u2500".repeat(60));
|
|
3613
|
-
console.log("\u{1F512} FREE TIER - Limited Preview");
|
|
3614
|
-
console.log("\u2500".repeat(60));
|
|
3615
|
-
console.log(`Found ${result2.teaser.totalKeywordsFound} keywords`);
|
|
3616
|
-
console.log(`Top keywords: ${result2.teaser.topKeywords.join(", ")}`);
|
|
3617
|
-
console.log(`
|
|
3618
|
-
${result2.teaser.upgradeMessage}`);
|
|
3619
|
-
console.log('\n\u{1F4A1} Run "rankcli login" to access your paid plan features.\n');
|
|
3620
|
-
return;
|
|
3621
|
-
}
|
|
3622
|
-
displayAIKeywordResults(result2);
|
|
3623
|
-
return;
|
|
3624
|
-
} else {
|
|
3625
|
-
const errorText = await response.text();
|
|
3626
|
-
console.log(`\u26A0\uFE0F Cloud request failed, running locally...
|
|
3627
|
-
`);
|
|
3628
|
-
}
|
|
3629
|
-
} catch (err) {
|
|
3630
|
-
console.log("\u26A0\uFE0F Cloud unavailable, running locally...\n");
|
|
3631
|
-
}
|
|
3633
|
+
const resolution = await resolveAIProvider(options);
|
|
3634
|
+
if (!resolution.ok) {
|
|
3635
|
+
console.log(`\u274C ${resolution.error}`);
|
|
3636
|
+
for (const hint of resolution.hints) console.log(` ${hint}`);
|
|
3637
|
+
console.log("");
|
|
3638
|
+
process.exitCode = 1;
|
|
3639
|
+
return;
|
|
3632
3640
|
}
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3641
|
+
const { ai } = resolution;
|
|
3642
|
+
if (ai.notice) console.log(ai.notice);
|
|
3643
|
+
const check = await completeWithAI(ai, { prompt: 'Reply with the JSON object {"ok": true} and nothing else.', maxTokens: 50, json: true });
|
|
3644
|
+
if ("error" in check) {
|
|
3645
|
+
console.log(`\u274C ${check.error}`);
|
|
3646
|
+
console.log(" Choose another provider with --ai-provider, or fix the key or model and try again.\n");
|
|
3647
|
+
process.exitCode = 1;
|
|
3636
3648
|
return;
|
|
3637
3649
|
}
|
|
3638
|
-
console.log(
|
|
3650
|
+
console.log(`\u{1F4CD} Analyzing: ${url}`);
|
|
3651
|
+
console.log(`\u{1F9E0} Model: ${ai.label} (${ai.model})
|
|
3652
|
+
`);
|
|
3653
|
+
console.log(`\u{1F52C} Running AI analysis (${ai.local ? "a few minutes on a local model" : "this may take a minute"})...
|
|
3654
|
+
`);
|
|
3639
3655
|
const result = await runAIKeywordResearch({
|
|
3640
3656
|
url,
|
|
3641
3657
|
tier: "solo",
|
|
3642
|
-
|
|
3658
|
+
llm: keywordLLMFromAI(ai),
|
|
3643
3659
|
enableEnhancedToolIdeas: true,
|
|
3644
3660
|
competitiveSearchKeys: {
|
|
3645
3661
|
braveApiKey: process.env.BRAVE_API_KEY,
|
|
@@ -3652,6 +3668,7 @@ ${result2.teaser.upgradeMessage}`);
|
|
|
3652
3668
|
console.log(`
|
|
3653
3669
|
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
3654
3670
|
`);
|
|
3671
|
+
process.exitCode = 1;
|
|
3655
3672
|
}
|
|
3656
3673
|
}
|
|
3657
3674
|
function displayAIKeywordResults(result) {
|
|
@@ -4078,7 +4095,7 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
4078
4095
|
}
|
|
4079
4096
|
|
|
4080
4097
|
// src/version.ts
|
|
4081
|
-
var VERSION = "0.0.
|
|
4098
|
+
var VERSION = "0.0.33";
|
|
4082
4099
|
|
|
4083
4100
|
// src/index.ts
|
|
4084
4101
|
var program = new Command();
|
|
@@ -4109,11 +4126,13 @@ program.command("fix").description("Agentic SEO fixer - detect, fix, commit, and
|
|
|
4109
4126
|
}
|
|
4110
4127
|
});
|
|
4111
4128
|
program.command("geo").description("GEO analysis - Check AI search visibility (ChatGPT, Perplexity, Claude)").option("-u, --url <url>", "URL to analyze").option("-o, --output <format>", "Output format (json, console)", "console").action(geo);
|
|
4112
|
-
program.command("keywords").description("AI-powered keyword research with actionable suggestions").option("-u, --url <url>", "Your website URL").option("-s, --seed <keywords>", "Seed keywords (comma-separated)").option("--auto", "Auto-extract seed keywords from your page").option("--quick", "Quick mode - skip questions, use defaults").option("--ai", "
|
|
4129
|
+
program.command("keywords").description("AI-powered keyword research with actionable suggestions").option("-u, --url <url>", "Your website URL").option("-s, --seed <keywords>", "Seed keywords (comma-separated)").option("--auto", "Auto-extract seed keywords from your page").option("--quick", "Quick mode - skip questions, use defaults").option("--ai", "AI keyword research with free tool ideas, on your own provider (Apple Intelligence on a Mac that has it)").option("--ai-provider <provider>", "AI provider for --ai (auto-detected if omitted, or set RANKCLI_AI_PROVIDER)").option("--ai-key <key>", "AI provider API key (or the provider's env var)").option("--ai-model <model>", "Override the provider's default model").option("--ai-base-url <url>", "Any OpenAI-compatible server (implies --ai-provider custom)").option("--local", "No effect; --ai always runs locally on your provider (kept for old scripts)").option("--competitor", "Competitor gap analysis mode (SpyFu Kombat-style)").option("-c, --competitors <domains>", "Competitor domains (comma-separated)").action(async (options) => {
|
|
4113
4130
|
if (options.ai && options.url) {
|
|
4114
4131
|
await keywordsAI(options.url, {
|
|
4115
|
-
|
|
4116
|
-
|
|
4132
|
+
aiProvider: options.aiProvider,
|
|
4133
|
+
aiKey: options.aiKey,
|
|
4134
|
+
aiModel: options.aiModel,
|
|
4135
|
+
aiBaseUrl: options.aiBaseUrl
|
|
4117
4136
|
});
|
|
4118
4137
|
} else if (options.competitor && options.url && options.seed) {
|
|
4119
4138
|
const seeds = options.seed.split(",").map((s) => s.trim());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankcli/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
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.24",
|
|
49
49
|
"@supabase/supabase-js": "^2.98.0",
|
|
50
50
|
"chalk": "^5.3.0",
|
|
51
51
|
"commander": "^12.0.0",
|