@rankcli/cli 0.0.32 → 0.0.34
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 +87 -70
- package/dist/index.mjs +87 -70
- package/package.json +3 -3
- package/wasm/rankcli_audit_engine.d.ts +14 -1
- package/wasm/rankcli_audit_engine.js +103 -78
- package/wasm/rankcli_audit_engine_bg.wasm +0 -0
- package/wasm/rankcli_audit_engine_bg.wasm.d.ts +9 -11
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);
|
|
@@ -3077,8 +3085,7 @@ async function trackAdd(options) {
|
|
|
3077
3085
|
spinner.fail("Could not find or create project");
|
|
3078
3086
|
return { success: false, error: "Project not found" };
|
|
3079
3087
|
}
|
|
3080
|
-
const
|
|
3081
|
-
const tier = planName?.toLowerCase() || "free";
|
|
3088
|
+
const tier = tierFromPlanId(getUserInfo().planId) ?? "free";
|
|
3082
3089
|
const { maxKeywords } = getTierLimits(tier);
|
|
3083
3090
|
const supabase = await getAuthenticatedClient();
|
|
3084
3091
|
if (!supabase) {
|
|
@@ -3238,7 +3245,7 @@ async function trackHistory(options) {
|
|
|
3238
3245
|
}
|
|
3239
3246
|
const days = options.days || 30;
|
|
3240
3247
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
3241
|
-
const { data: historyData, error } = await supabase.from("
|
|
3248
|
+
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
3249
|
if (error) {
|
|
3243
3250
|
spinner.fail(`Failed to load history: ${error.message}`);
|
|
3244
3251
|
return { success: false, error: error.message };
|
|
@@ -3247,7 +3254,7 @@ async function trackHistory(options) {
|
|
|
3247
3254
|
const history = (historyData || []).map((row) => ({
|
|
3248
3255
|
position: row.position,
|
|
3249
3256
|
url: row.url,
|
|
3250
|
-
recordedAt: new Date(row.
|
|
3257
|
+
recordedAt: new Date(row.checked_at)
|
|
3251
3258
|
}));
|
|
3252
3259
|
if (options.output === "json") {
|
|
3253
3260
|
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
@@ -3255,7 +3262,9 @@ async function trackHistory(options) {
|
|
|
3255
3262
|
console.log("\n" + import_chalk6.default.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
3256
3263
|
console.log("\u2550".repeat(50));
|
|
3257
3264
|
if (history.length === 0) {
|
|
3258
|
-
console.log(import_chalk6.default.dim(
|
|
3265
|
+
console.log(import_chalk6.default.dim(`
|
|
3266
|
+
No checks recorded yet. Run 'rankcli track check --url <your site>' to record one.
|
|
3267
|
+
`));
|
|
3259
3268
|
} else {
|
|
3260
3269
|
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
3261
3270
|
const sparkline = positions.map((p) => {
|
|
@@ -3266,7 +3275,7 @@ async function trackHistory(options) {
|
|
|
3266
3275
|
}).join("");
|
|
3267
3276
|
console.log(`
|
|
3268
3277
|
${sparkline} (last ${positions.length} checks)`);
|
|
3269
|
-
console.log(import_chalk6.default.dim(" \u2190 newer
|
|
3278
|
+
console.log(import_chalk6.default.dim(" \u2190 older newer \u2192\n"));
|
|
3270
3279
|
const recent = history.slice(0, 10);
|
|
3271
3280
|
for (const entry of recent) {
|
|
3272
3281
|
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
@@ -3357,8 +3366,7 @@ async function trackCheck(options) {
|
|
|
3357
3366
|
if (!project) {
|
|
3358
3367
|
return { success: false, error: "Project not found" };
|
|
3359
3368
|
}
|
|
3360
|
-
const
|
|
3361
|
-
const tier = planName?.toLowerCase() || "free";
|
|
3369
|
+
const tier = tierFromPlanId(getUserInfo().planId) ?? "free";
|
|
3362
3370
|
const { checksPerDay } = getTierLimits(tier);
|
|
3363
3371
|
if (!options.force && project.lastRankCheckAt) {
|
|
3364
3372
|
const hoursSinceCheck = (Date.now() - project.lastRankCheckAt.getTime()) / (1e3 * 60 * 60);
|
|
@@ -3580,6 +3588,40 @@ async function geo(options) {
|
|
|
3580
3588
|
// src/commands/keywords.ts
|
|
3581
3589
|
var readline2 = __toESM(require("readline"));
|
|
3582
3590
|
var import_agent_runtime7 = require("@rankcli/agent-runtime");
|
|
3591
|
+
|
|
3592
|
+
// src/lib/keyword-llm.ts
|
|
3593
|
+
async function openAIEmbeddings(ai, texts) {
|
|
3594
|
+
const response = await fetch(`${ai.baseUrl}/embeddings`, {
|
|
3595
|
+
method: "POST",
|
|
3596
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ai.apiKey}` },
|
|
3597
|
+
body: JSON.stringify({ model: "text-embedding-3-small", input: texts }),
|
|
3598
|
+
signal: AbortSignal.timeout(6e4)
|
|
3599
|
+
});
|
|
3600
|
+
if (!response.ok) throw new Error(`OpenAI embeddings returned HTTP ${response.status}`);
|
|
3601
|
+
const data = await response.json();
|
|
3602
|
+
return data.data.map((item) => item.embedding);
|
|
3603
|
+
}
|
|
3604
|
+
function keywordLLMFromAI(ai) {
|
|
3605
|
+
const llm = {
|
|
3606
|
+
async complete(prompt, opts = {}) {
|
|
3607
|
+
const reply = await completeWithAI(ai, { system: opts.system, prompt, maxTokens: opts.maxTokens, json: true });
|
|
3608
|
+
if ("error" in reply) throw new Error(reply.error);
|
|
3609
|
+
return reply.text;
|
|
3610
|
+
}
|
|
3611
|
+
};
|
|
3612
|
+
if (ai.provider === "openai" && ai.apiKey && ai.jsonMode) {
|
|
3613
|
+
llm.embed = (texts) => openAIEmbeddings(ai, texts);
|
|
3614
|
+
}
|
|
3615
|
+
if (ai.provider === "apple") {
|
|
3616
|
+
llm.maxContentChars = 5e3;
|
|
3617
|
+
llm.maxToolIdeas = 5;
|
|
3618
|
+
} else if (ai.local) {
|
|
3619
|
+
llm.maxToolIdeas = 5;
|
|
3620
|
+
}
|
|
3621
|
+
return llm;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
// src/commands/keywords.ts
|
|
3583
3625
|
async function keywords(options) {
|
|
3584
3626
|
console.log("\n\u{1F511} RankCLI - Keyword Research\n");
|
|
3585
3627
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
@@ -3740,65 +3782,37 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
3740
3782
|
`);
|
|
3741
3783
|
}
|
|
3742
3784
|
}
|
|
3743
|
-
var SUPABASE_URL = "https://eqzlmjbvrtrglknphdai.supabase.co";
|
|
3744
3785
|
async function keywordsAI(url, options = {}) {
|
|
3745
3786
|
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
3787
|
try {
|
|
3750
3788
|
const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
|
|
3751
3789
|
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
|
-
}
|
|
3790
|
+
const resolution = await resolveAIProvider(options);
|
|
3791
|
+
if (!resolution.ok) {
|
|
3792
|
+
console.log(`\u274C ${resolution.error}`);
|
|
3793
|
+
for (const hint of resolution.hints) console.log(` ${hint}`);
|
|
3794
|
+
console.log("");
|
|
3795
|
+
process.exitCode = 1;
|
|
3796
|
+
return;
|
|
3791
3797
|
}
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3798
|
+
const { ai } = resolution;
|
|
3799
|
+
if (ai.notice) console.log(ai.notice);
|
|
3800
|
+
const check = await completeWithAI(ai, { prompt: 'Reply with the JSON object {"ok": true} and nothing else.', maxTokens: 50, json: true });
|
|
3801
|
+
if ("error" in check) {
|
|
3802
|
+
console.log(`\u274C ${check.error}`);
|
|
3803
|
+
console.log(" Choose another provider with --ai-provider, or fix the key or model and try again.\n");
|
|
3804
|
+
process.exitCode = 1;
|
|
3795
3805
|
return;
|
|
3796
3806
|
}
|
|
3797
|
-
console.log(
|
|
3807
|
+
console.log(`\u{1F4CD} Analyzing: ${url}`);
|
|
3808
|
+
console.log(`\u{1F9E0} Model: ${ai.label} (${ai.model})
|
|
3809
|
+
`);
|
|
3810
|
+
console.log(`\u{1F52C} Running AI analysis (${ai.local ? "a few minutes on a local model" : "this may take a minute"})...
|
|
3811
|
+
`);
|
|
3798
3812
|
const result = await (0, import_agent_runtime7.runAIKeywordResearch)({
|
|
3799
3813
|
url,
|
|
3800
3814
|
tier: "solo",
|
|
3801
|
-
|
|
3815
|
+
llm: keywordLLMFromAI(ai),
|
|
3802
3816
|
enableEnhancedToolIdeas: true,
|
|
3803
3817
|
competitiveSearchKeys: {
|
|
3804
3818
|
braveApiKey: process.env.BRAVE_API_KEY,
|
|
@@ -3811,6 +3825,7 @@ ${result2.teaser.upgradeMessage}`);
|
|
|
3811
3825
|
console.log(`
|
|
3812
3826
|
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
3813
3827
|
`);
|
|
3828
|
+
process.exitCode = 1;
|
|
3814
3829
|
}
|
|
3815
3830
|
}
|
|
3816
3831
|
function displayAIKeywordResults(result) {
|
|
@@ -4222,7 +4237,7 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
4222
4237
|
}
|
|
4223
4238
|
|
|
4224
4239
|
// src/version.ts
|
|
4225
|
-
var VERSION = "0.0.
|
|
4240
|
+
var VERSION = "0.0.34";
|
|
4226
4241
|
|
|
4227
4242
|
// src/index.ts
|
|
4228
4243
|
var program = new import_commander.Command();
|
|
@@ -4253,11 +4268,13 @@ program.command("fix").description("Agentic SEO fixer - detect, fix, commit, and
|
|
|
4253
4268
|
}
|
|
4254
4269
|
});
|
|
4255
4270
|
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", "
|
|
4271
|
+
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
4272
|
if (options.ai && options.url) {
|
|
4258
4273
|
await keywordsAI(options.url, {
|
|
4259
|
-
|
|
4260
|
-
|
|
4274
|
+
aiProvider: options.aiProvider,
|
|
4275
|
+
aiKey: options.aiKey,
|
|
4276
|
+
aiModel: options.aiModel,
|
|
4277
|
+
aiBaseUrl: options.aiBaseUrl
|
|
4261
4278
|
});
|
|
4262
4279
|
} else if (options.competitor && options.url && options.seed) {
|
|
4263
4280
|
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);
|
|
@@ -2908,8 +2916,7 @@ async function trackAdd(options) {
|
|
|
2908
2916
|
spinner.fail("Could not find or create project");
|
|
2909
2917
|
return { success: false, error: "Project not found" };
|
|
2910
2918
|
}
|
|
2911
|
-
const
|
|
2912
|
-
const tier = planName?.toLowerCase() || "free";
|
|
2919
|
+
const tier = tierFromPlanId(getUserInfo().planId) ?? "free";
|
|
2913
2920
|
const { maxKeywords } = getTierLimits(tier);
|
|
2914
2921
|
const supabase = await getAuthenticatedClient();
|
|
2915
2922
|
if (!supabase) {
|
|
@@ -3069,7 +3076,7 @@ async function trackHistory(options) {
|
|
|
3069
3076
|
}
|
|
3070
3077
|
const days = options.days || 30;
|
|
3071
3078
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
3072
|
-
const { data: historyData, error } = await supabase.from("
|
|
3079
|
+
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
3080
|
if (error) {
|
|
3074
3081
|
spinner.fail(`Failed to load history: ${error.message}`);
|
|
3075
3082
|
return { success: false, error: error.message };
|
|
@@ -3078,7 +3085,7 @@ async function trackHistory(options) {
|
|
|
3078
3085
|
const history = (historyData || []).map((row) => ({
|
|
3079
3086
|
position: row.position,
|
|
3080
3087
|
url: row.url,
|
|
3081
|
-
recordedAt: new Date(row.
|
|
3088
|
+
recordedAt: new Date(row.checked_at)
|
|
3082
3089
|
}));
|
|
3083
3090
|
if (options.output === "json") {
|
|
3084
3091
|
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
@@ -3086,7 +3093,9 @@ async function trackHistory(options) {
|
|
|
3086
3093
|
console.log("\n" + chalk6.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
3087
3094
|
console.log("\u2550".repeat(50));
|
|
3088
3095
|
if (history.length === 0) {
|
|
3089
|
-
console.log(chalk6.dim(
|
|
3096
|
+
console.log(chalk6.dim(`
|
|
3097
|
+
No checks recorded yet. Run 'rankcli track check --url <your site>' to record one.
|
|
3098
|
+
`));
|
|
3090
3099
|
} else {
|
|
3091
3100
|
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
3092
3101
|
const sparkline = positions.map((p) => {
|
|
@@ -3097,7 +3106,7 @@ async function trackHistory(options) {
|
|
|
3097
3106
|
}).join("");
|
|
3098
3107
|
console.log(`
|
|
3099
3108
|
${sparkline} (last ${positions.length} checks)`);
|
|
3100
|
-
console.log(chalk6.dim(" \u2190 newer
|
|
3109
|
+
console.log(chalk6.dim(" \u2190 older newer \u2192\n"));
|
|
3101
3110
|
const recent = history.slice(0, 10);
|
|
3102
3111
|
for (const entry of recent) {
|
|
3103
3112
|
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
@@ -3188,8 +3197,7 @@ async function trackCheck(options) {
|
|
|
3188
3197
|
if (!project) {
|
|
3189
3198
|
return { success: false, error: "Project not found" };
|
|
3190
3199
|
}
|
|
3191
|
-
const
|
|
3192
|
-
const tier = planName?.toLowerCase() || "free";
|
|
3200
|
+
const tier = tierFromPlanId(getUserInfo().planId) ?? "free";
|
|
3193
3201
|
const { checksPerDay } = getTierLimits(tier);
|
|
3194
3202
|
if (!options.force && project.lastRankCheckAt) {
|
|
3195
3203
|
const hoursSinceCheck = (Date.now() - project.lastRankCheckAt.getTime()) / (1e3 * 60 * 60);
|
|
@@ -3421,6 +3429,40 @@ import {
|
|
|
3421
3429
|
formatTopicReport,
|
|
3422
3430
|
runAIKeywordResearch
|
|
3423
3431
|
} from "@rankcli/agent-runtime";
|
|
3432
|
+
|
|
3433
|
+
// src/lib/keyword-llm.ts
|
|
3434
|
+
async function openAIEmbeddings(ai, texts) {
|
|
3435
|
+
const response = await fetch(`${ai.baseUrl}/embeddings`, {
|
|
3436
|
+
method: "POST",
|
|
3437
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ai.apiKey}` },
|
|
3438
|
+
body: JSON.stringify({ model: "text-embedding-3-small", input: texts }),
|
|
3439
|
+
signal: AbortSignal.timeout(6e4)
|
|
3440
|
+
});
|
|
3441
|
+
if (!response.ok) throw new Error(`OpenAI embeddings returned HTTP ${response.status}`);
|
|
3442
|
+
const data = await response.json();
|
|
3443
|
+
return data.data.map((item) => item.embedding);
|
|
3444
|
+
}
|
|
3445
|
+
function keywordLLMFromAI(ai) {
|
|
3446
|
+
const llm = {
|
|
3447
|
+
async complete(prompt, opts = {}) {
|
|
3448
|
+
const reply = await completeWithAI(ai, { system: opts.system, prompt, maxTokens: opts.maxTokens, json: true });
|
|
3449
|
+
if ("error" in reply) throw new Error(reply.error);
|
|
3450
|
+
return reply.text;
|
|
3451
|
+
}
|
|
3452
|
+
};
|
|
3453
|
+
if (ai.provider === "openai" && ai.apiKey && ai.jsonMode) {
|
|
3454
|
+
llm.embed = (texts) => openAIEmbeddings(ai, texts);
|
|
3455
|
+
}
|
|
3456
|
+
if (ai.provider === "apple") {
|
|
3457
|
+
llm.maxContentChars = 5e3;
|
|
3458
|
+
llm.maxToolIdeas = 5;
|
|
3459
|
+
} else if (ai.local) {
|
|
3460
|
+
llm.maxToolIdeas = 5;
|
|
3461
|
+
}
|
|
3462
|
+
return llm;
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
// src/commands/keywords.ts
|
|
3424
3466
|
async function keywords(options) {
|
|
3425
3467
|
console.log("\n\u{1F511} RankCLI - Keyword Research\n");
|
|
3426
3468
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
@@ -3581,65 +3623,37 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
3581
3623
|
`);
|
|
3582
3624
|
}
|
|
3583
3625
|
}
|
|
3584
|
-
var SUPABASE_URL = "https://eqzlmjbvrtrglknphdai.supabase.co";
|
|
3585
3626
|
async function keywordsAI(url, options = {}) {
|
|
3586
3627
|
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
3628
|
try {
|
|
3591
3629
|
const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
|
|
3592
3630
|
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
|
-
}
|
|
3631
|
+
const resolution = await resolveAIProvider(options);
|
|
3632
|
+
if (!resolution.ok) {
|
|
3633
|
+
console.log(`\u274C ${resolution.error}`);
|
|
3634
|
+
for (const hint of resolution.hints) console.log(` ${hint}`);
|
|
3635
|
+
console.log("");
|
|
3636
|
+
process.exitCode = 1;
|
|
3637
|
+
return;
|
|
3632
3638
|
}
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3639
|
+
const { ai } = resolution;
|
|
3640
|
+
if (ai.notice) console.log(ai.notice);
|
|
3641
|
+
const check = await completeWithAI(ai, { prompt: 'Reply with the JSON object {"ok": true} and nothing else.', maxTokens: 50, json: true });
|
|
3642
|
+
if ("error" in check) {
|
|
3643
|
+
console.log(`\u274C ${check.error}`);
|
|
3644
|
+
console.log(" Choose another provider with --ai-provider, or fix the key or model and try again.\n");
|
|
3645
|
+
process.exitCode = 1;
|
|
3636
3646
|
return;
|
|
3637
3647
|
}
|
|
3638
|
-
console.log(
|
|
3648
|
+
console.log(`\u{1F4CD} Analyzing: ${url}`);
|
|
3649
|
+
console.log(`\u{1F9E0} Model: ${ai.label} (${ai.model})
|
|
3650
|
+
`);
|
|
3651
|
+
console.log(`\u{1F52C} Running AI analysis (${ai.local ? "a few minutes on a local model" : "this may take a minute"})...
|
|
3652
|
+
`);
|
|
3639
3653
|
const result = await runAIKeywordResearch({
|
|
3640
3654
|
url,
|
|
3641
3655
|
tier: "solo",
|
|
3642
|
-
|
|
3656
|
+
llm: keywordLLMFromAI(ai),
|
|
3643
3657
|
enableEnhancedToolIdeas: true,
|
|
3644
3658
|
competitiveSearchKeys: {
|
|
3645
3659
|
braveApiKey: process.env.BRAVE_API_KEY,
|
|
@@ -3652,6 +3666,7 @@ ${result2.teaser.upgradeMessage}`);
|
|
|
3652
3666
|
console.log(`
|
|
3653
3667
|
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
3654
3668
|
`);
|
|
3669
|
+
process.exitCode = 1;
|
|
3655
3670
|
}
|
|
3656
3671
|
}
|
|
3657
3672
|
function displayAIKeywordResults(result) {
|
|
@@ -4078,7 +4093,7 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
4078
4093
|
}
|
|
4079
4094
|
|
|
4080
4095
|
// src/version.ts
|
|
4081
|
-
var VERSION = "0.0.
|
|
4096
|
+
var VERSION = "0.0.34";
|
|
4082
4097
|
|
|
4083
4098
|
// src/index.ts
|
|
4084
4099
|
var program = new Command();
|
|
@@ -4109,11 +4124,13 @@ program.command("fix").description("Agentic SEO fixer - detect, fix, commit, and
|
|
|
4109
4124
|
}
|
|
4110
4125
|
});
|
|
4111
4126
|
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", "
|
|
4127
|
+
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
4128
|
if (options.ai && options.url) {
|
|
4114
4129
|
await keywordsAI(options.url, {
|
|
4115
|
-
|
|
4116
|
-
|
|
4130
|
+
aiProvider: options.aiProvider,
|
|
4131
|
+
aiKey: options.aiKey,
|
|
4132
|
+
aiModel: options.aiModel,
|
|
4133
|
+
aiBaseUrl: options.aiBaseUrl
|
|
4117
4134
|
});
|
|
4118
4135
|
} else if (options.competitor && options.url && options.seed) {
|
|
4119
4136
|
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.34",
|
|
4
4
|
"description": "RankCLI - Ship code, get ranked. SEO meets CI/CD.",
|
|
5
5
|
"homepage": "https://rankcli.dev",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"build": "npm run build:checks && npm run build:wasm && npm run build:js",
|
|
34
34
|
"build:js": "npm run update-version && tsup src/index.ts src/wasm-engine.ts --format cjs,esm --dts",
|
|
35
35
|
"build:checks": "test -d ../agent-runtime && (cd ../agent-runtime && pnpm build:wasm) || echo 'agent-runtime not present as a monorepo sibling - build.rs will fall back to the node_modules-installed bundle'",
|
|
36
|
-
"build:wasm": "cd native && wasm-pack build --target nodejs --release && cp -r pkg ../wasm",
|
|
36
|
+
"build:wasm": "cd native && wasm-pack build --target nodejs --release && mkdir -p ../wasm && cp -r pkg/* ../wasm/ && node ../scripts/verify-wasm.js",
|
|
37
37
|
"build:wasm:dev": "cd native && wasm-pack build --target nodejs --dev && mkdir -p ../wasm && cp -r pkg/* ../wasm/",
|
|
38
38
|
"prebuild:wasm": "mkdir -p wasm",
|
|
39
39
|
"inject-config": "node scripts/inject-config.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",
|
|
@@ -24,8 +24,21 @@ export function get_encrypted_size(): number;
|
|
|
24
24
|
export function get_tier(): string;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Initialize the engine with
|
|
27
|
+
* Initialize the engine with the build-time decrypt passphrase.
|
|
28
28
|
* Decrypts the embedded checks and stores them for execution.
|
|
29
|
+
*
|
|
30
|
+
* The `license_key` parameter is misleadingly named for what it does
|
|
31
|
+
* now: entitlement (is this user actually paid?) is decided in
|
|
32
|
+
* TypeScript, from the real Supabase-backed apiKey/plan the CLI already
|
|
33
|
+
* validates on login - not by anything checkable here. The caller only
|
|
34
|
+
* invokes this at all once it has confirmed a paid apiKey, always
|
|
35
|
+
* passing the single build-embedded passphrase (see
|
|
36
|
+
* scripts/inject-config.js), never a user-supplied string. This
|
|
37
|
+
* decryption step exists purely to keep the premium check source out of
|
|
38
|
+
* static analysis of the shipped binary; get_tier()/get_checks_limit()/
|
|
39
|
+
* validate_license() below are legacy from an earlier signed-license
|
|
40
|
+
* design that was never finished and should not be trusted as an
|
|
41
|
+
* entitlement source.
|
|
29
42
|
*/
|
|
30
43
|
export function init_engine(license_key: string): boolean;
|
|
31
44
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/* @ts-self-types="./rankcli_audit_engine.d.ts" */
|
|
2
2
|
|
|
3
|
-
//#region exports
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* Get checks limit for current tier
|
|
7
5
|
* @returns {number}
|
|
@@ -22,18 +20,24 @@ function get_decrypted_code() {
|
|
|
22
20
|
let deferred2_0;
|
|
23
21
|
let deferred2_1;
|
|
24
22
|
try {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
var
|
|
28
|
-
|
|
23
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
24
|
+
wasm.get_decrypted_code(retptr);
|
|
25
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
26
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
27
|
+
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
|
28
|
+
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
|
|
29
|
+
var ptr1 = r0;
|
|
30
|
+
var len1 = r1;
|
|
31
|
+
if (r3) {
|
|
29
32
|
ptr1 = 0; len1 = 0;
|
|
30
|
-
throw
|
|
33
|
+
throw takeObject(r2);
|
|
31
34
|
}
|
|
32
35
|
deferred2_0 = ptr1;
|
|
33
36
|
deferred2_1 = len1;
|
|
34
37
|
return getStringFromWasm0(ptr1, len1);
|
|
35
38
|
} finally {
|
|
36
|
-
wasm.
|
|
39
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
40
|
+
wasm.__wbindgen_export(deferred2_0, deferred2_1, 1);
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
exports.get_decrypted_code = get_decrypted_code;
|
|
@@ -56,30 +60,55 @@ function get_tier() {
|
|
|
56
60
|
let deferred1_0;
|
|
57
61
|
let deferred1_1;
|
|
58
62
|
try {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
64
|
+
wasm.get_tier(retptr);
|
|
65
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
66
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
67
|
+
deferred1_0 = r0;
|
|
68
|
+
deferred1_1 = r1;
|
|
69
|
+
return getStringFromWasm0(r0, r1);
|
|
63
70
|
} finally {
|
|
64
|
-
wasm.
|
|
71
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
72
|
+
wasm.__wbindgen_export(deferred1_0, deferred1_1, 1);
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
exports.get_tier = get_tier;
|
|
68
76
|
|
|
69
77
|
/**
|
|
70
|
-
* Initialize the engine with
|
|
78
|
+
* Initialize the engine with the build-time decrypt passphrase.
|
|
71
79
|
* Decrypts the embedded checks and stores them for execution.
|
|
80
|
+
*
|
|
81
|
+
* The `license_key` parameter is misleadingly named for what it does
|
|
82
|
+
* now: entitlement (is this user actually paid?) is decided in
|
|
83
|
+
* TypeScript, from the real Supabase-backed apiKey/plan the CLI already
|
|
84
|
+
* validates on login - not by anything checkable here. The caller only
|
|
85
|
+
* invokes this at all once it has confirmed a paid apiKey, always
|
|
86
|
+
* passing the single build-embedded passphrase (see
|
|
87
|
+
* scripts/inject-config.js), never a user-supplied string. This
|
|
88
|
+
* decryption step exists purely to keep the premium check source out of
|
|
89
|
+
* static analysis of the shipped binary; get_tier()/get_checks_limit()/
|
|
90
|
+
* validate_license() below are legacy from an earlier signed-license
|
|
91
|
+
* design that was never finished and should not be trusted as an
|
|
92
|
+
* entitlement source.
|
|
72
93
|
* @param {string} license_key
|
|
73
94
|
* @returns {boolean}
|
|
74
95
|
*/
|
|
75
96
|
function init_engine(license_key) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
try {
|
|
98
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
99
|
+
const ptr0 = passStringToWasm0(license_key, wasm.__wbindgen_export2, wasm.__wbindgen_export3);
|
|
100
|
+
const len0 = WASM_VECTOR_LEN;
|
|
101
|
+
wasm.init_engine(retptr, ptr0, len0);
|
|
102
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
103
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
104
|
+
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
|
105
|
+
if (r2) {
|
|
106
|
+
throw takeObject(r1);
|
|
107
|
+
}
|
|
108
|
+
return r0 !== 0;
|
|
109
|
+
} finally {
|
|
110
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
81
111
|
}
|
|
82
|
-
return ret[0] !== 0;
|
|
83
112
|
}
|
|
84
113
|
exports.init_engine = init_engine;
|
|
85
114
|
|
|
@@ -102,14 +131,18 @@ function validate_license(license_key) {
|
|
|
102
131
|
let deferred2_0;
|
|
103
132
|
let deferred2_1;
|
|
104
133
|
try {
|
|
105
|
-
const
|
|
134
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
135
|
+
const ptr0 = passStringToWasm0(license_key, wasm.__wbindgen_export2, wasm.__wbindgen_export3);
|
|
106
136
|
const len0 = WASM_VECTOR_LEN;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
wasm.validate_license(retptr, ptr0, len0);
|
|
138
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
139
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
140
|
+
deferred2_0 = r0;
|
|
141
|
+
deferred2_1 = r1;
|
|
142
|
+
return getStringFromWasm0(r0, r1);
|
|
111
143
|
} finally {
|
|
112
|
-
wasm.
|
|
144
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
145
|
+
wasm.__wbindgen_export(deferred2_0, deferred2_1, 1);
|
|
113
146
|
}
|
|
114
147
|
}
|
|
115
148
|
exports.validate_license = validate_license;
|
|
@@ -122,39 +155,30 @@ function version() {
|
|
|
122
155
|
let deferred1_0;
|
|
123
156
|
let deferred1_1;
|
|
124
157
|
try {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
158
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
159
|
+
wasm.version(retptr);
|
|
160
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
161
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
162
|
+
deferred1_0 = r0;
|
|
163
|
+
deferred1_1 = r1;
|
|
164
|
+
return getStringFromWasm0(r0, r1);
|
|
129
165
|
} finally {
|
|
130
|
-
wasm.
|
|
166
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
167
|
+
wasm.__wbindgen_export(deferred1_0, deferred1_1, 1);
|
|
131
168
|
}
|
|
132
169
|
}
|
|
133
170
|
exports.version = version;
|
|
134
171
|
|
|
135
|
-
//#endregion
|
|
136
|
-
|
|
137
|
-
//#region wasm imports
|
|
138
|
-
|
|
139
172
|
function __wbg_get_imports() {
|
|
140
173
|
const import0 = {
|
|
141
174
|
__proto__: null,
|
|
142
|
-
__wbg_Error_8c4e43fe74559d73: function(
|
|
175
|
+
__wbg_Error_8c4e43fe74559d73: function(arg0, arg1) {
|
|
143
176
|
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
144
|
-
return ret;
|
|
145
|
-
},
|
|
177
|
+
return addHeapObject(ret);
|
|
178
|
+
},
|
|
146
179
|
__wbg___wbindgen_throw_be289d5034ed271b: function(arg0, arg1) {
|
|
147
180
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
148
181
|
},
|
|
149
|
-
__wbindgen_init_externref_table: function() {
|
|
150
|
-
const table = wasm.__wbindgen_externrefs;
|
|
151
|
-
const offset = table.grow(4);
|
|
152
|
-
table.set(0, undefined);
|
|
153
|
-
table.set(offset + 0, undefined);
|
|
154
|
-
table.set(offset + 1, null);
|
|
155
|
-
table.set(offset + 2, true);
|
|
156
|
-
table.set(offset + 3, false);
|
|
157
|
-
},
|
|
158
182
|
};
|
|
159
183
|
return {
|
|
160
184
|
__proto__: null,
|
|
@@ -162,10 +186,29 @@ function __wbg_get_imports() {
|
|
|
162
186
|
};
|
|
163
187
|
}
|
|
164
188
|
|
|
189
|
+
function addHeapObject(obj) {
|
|
190
|
+
if (heap_next === heap.length) heap.push(heap.length + 1);
|
|
191
|
+
const idx = heap_next;
|
|
192
|
+
heap_next = heap[idx];
|
|
193
|
+
|
|
194
|
+
heap[idx] = obj;
|
|
195
|
+
return idx;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function dropObject(idx) {
|
|
199
|
+
if (idx < 132) return;
|
|
200
|
+
heap[idx] = heap_next;
|
|
201
|
+
heap_next = idx;
|
|
202
|
+
}
|
|
165
203
|
|
|
166
|
-
|
|
204
|
+
let cachedDataViewMemory0 = null;
|
|
205
|
+
function getDataViewMemory0() {
|
|
206
|
+
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
|
207
|
+
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
|
208
|
+
}
|
|
209
|
+
return cachedDataViewMemory0;
|
|
210
|
+
}
|
|
167
211
|
|
|
168
|
-
//#region intrinsics
|
|
169
212
|
function getStringFromWasm0(ptr, len) {
|
|
170
213
|
ptr = ptr >>> 0;
|
|
171
214
|
return decodeText(ptr, len);
|
|
@@ -179,24 +222,14 @@ function getUint8ArrayMemory0() {
|
|
|
179
222
|
return cachedUint8ArrayMemory0;
|
|
180
223
|
}
|
|
181
224
|
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return e instanceof Error ? `${e.message}\n\nStack:\n${e.stack}` : e.toString();
|
|
189
|
-
} catch(_) {
|
|
190
|
-
return "<failed to stringify thrown value>";
|
|
191
|
-
}
|
|
192
|
-
}());
|
|
193
|
-
console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", error);
|
|
194
|
-
throw e;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
225
|
+
function getObject(idx) { return heap[idx]; }
|
|
226
|
+
|
|
227
|
+
let heap = new Array(128).fill(undefined);
|
|
228
|
+
heap.push(undefined, null, true, false);
|
|
229
|
+
|
|
230
|
+
let heap_next = heap.length;
|
|
197
231
|
|
|
198
232
|
function passStringToWasm0(arg, malloc, realloc) {
|
|
199
|
-
if (typeof(arg) !== 'string') throw new Error(`expected a string argument, found ${typeof(arg)}`);
|
|
200
233
|
if (realloc === undefined) {
|
|
201
234
|
const buf = cachedTextEncoder.encode(arg);
|
|
202
235
|
const ptr = malloc(buf.length, 1) >>> 0;
|
|
@@ -224,7 +257,7 @@ function passStringToWasm0(arg, malloc, realloc) {
|
|
|
224
257
|
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
|
225
258
|
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
226
259
|
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
227
|
-
|
|
260
|
+
|
|
228
261
|
offset += ret.written;
|
|
229
262
|
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
230
263
|
}
|
|
@@ -233,10 +266,10 @@ function passStringToWasm0(arg, malloc, realloc) {
|
|
|
233
266
|
return ptr;
|
|
234
267
|
}
|
|
235
268
|
|
|
236
|
-
function
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
return
|
|
269
|
+
function takeObject(idx) {
|
|
270
|
+
const ret = getObject(idx);
|
|
271
|
+
dropObject(idx);
|
|
272
|
+
return ret;
|
|
240
273
|
}
|
|
241
274
|
|
|
242
275
|
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
@@ -260,15 +293,7 @@ if (!('encodeInto' in cachedTextEncoder)) {
|
|
|
260
293
|
|
|
261
294
|
let WASM_VECTOR_LEN = 0;
|
|
262
295
|
|
|
263
|
-
|
|
264
|
-
//#endregion
|
|
265
|
-
|
|
266
|
-
//#region wasm loading
|
|
267
296
|
const wasmPath = `${__dirname}/rankcli_audit_engine_bg.wasm`;
|
|
268
297
|
const wasmBytes = require('fs').readFileSync(wasmPath);
|
|
269
298
|
const wasmModule = new WebAssembly.Module(wasmBytes);
|
|
270
299
|
const wasm = new WebAssembly.Instance(wasmModule, __wbg_get_imports()).exports;
|
|
271
|
-
wasm.__wbindgen_start();
|
|
272
|
-
|
|
273
|
-
//#endregion
|
|
274
|
-
exports.__wasm = wasm;
|
|
Binary file
|
|
@@ -2,16 +2,14 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
export const memory: WebAssembly.Memory;
|
|
4
4
|
export const get_checks_limit: () => number;
|
|
5
|
-
export const get_decrypted_code: () =>
|
|
5
|
+
export const get_decrypted_code: (a: number) => void;
|
|
6
6
|
export const get_encrypted_size: () => number;
|
|
7
|
-
export const get_tier: () =>
|
|
8
|
-
export const init_engine: (a: number, b: number
|
|
7
|
+
export const get_tier: (a: number) => void;
|
|
8
|
+
export const init_engine: (a: number, b: number, c: number) => void;
|
|
9
9
|
export const is_initialized: () => number;
|
|
10
|
-
export const validate_license: (a: number, b: number) =>
|
|
11
|
-
export const version: () =>
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
16
|
-
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
17
|
-
export const __wbindgen_start: () => void;
|
|
10
|
+
export const validate_license: (a: number, b: number, c: number) => void;
|
|
11
|
+
export const version: (a: number) => void;
|
|
12
|
+
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
13
|
+
export const __wbindgen_export: (a: number, b: number, c: number) => void;
|
|
14
|
+
export const __wbindgen_export2: (a: number, b: number) => number;
|
|
15
|
+
export const __wbindgen_export3: (a: number, b: number, c: number, d: number) => number;
|