@rankcli/cli 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,17 @@ Developer-first SEO. CLI-native. Optimizes for Google *and* ChatGPT, Perplexity,
16
16
 
17
17
  ## Installation
18
18
 
19
+ Standalone binary, no Node required:
20
+
21
+ ```bash
22
+ brew install integrallis/tap/rankcli
23
+ # or on Windows:
24
+ scoop bucket add integrallis https://github.com/integrallis/scoop-bucket
25
+ scoop install rankcli
26
+ ```
27
+
28
+ Or via npm (requires Node 18+):
29
+
19
30
  ```bash
20
31
  npm install -g @rankcli/cli
21
32
  # or
@@ -59,23 +70,22 @@ Analyzes:
59
70
 
60
71
  ### 🔌 MCP Server (For AI Assistants)
61
72
 
62
- Let Claude, Cursor, or any MCP-compatible AI run SEO audits for you. **No installation required** — just add your API key:
73
+ Let Claude, Cursor, or any MCP-compatible AI run SEO audits for you. **Free, local, no signup**:
63
74
 
64
75
  **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
65
76
  ```json
66
77
  {
67
78
  "mcpServers": {
68
79
  "rankcli": {
69
- "url": "https://rankcli-audit-worker.fly.dev/mcp",
70
- "headers": {
71
- "Authorization": "Bearer YOUR_API_KEY"
72
- }
80
+ "command": "npx",
81
+ "args": ["@rankcli/mcp-server"]
73
82
  }
74
83
  }
75
84
  }
76
85
  ```
77
86
 
78
- Get your API key at [rankcli.dev/dashboard](https://rankcli.dev/dashboard).
87
+ Nothing is sent to RankCLI's servers — see [`@rankcli/mcp-server`](https://www.npmjs.com/package/@rankcli/mcp-server).
88
+ A hosted endpoint (with your [API key](https://rankcli.dev/dashboard)) is also available for teams that want dashboard sync.
79
89
 
80
90
  **Available tools:**
81
91
  - `seo_audit` — Full 280+ check audit on any URL
@@ -207,12 +217,12 @@ jobs:
207
217
  | GEO Analysis | Basic | Full |
208
218
  | Framework Fixes | - | ✅ 25+ |
209
219
  | Auto-Fix PRs | - | ✅ |
210
- | Sites | 1 | 10 |
220
+ | Sites | 1 | 3+ |
211
221
 
212
222
  ## Links
213
223
 
214
224
  - **Docs**: [rankcli.dev/docs](https://rankcli.dev/docs)
215
- - **GitHub**: [github.com/rankcli/rankcli](https://github.com/rankcli/rankcli)
225
+ - **GitHub**: [github.com/integrallis/rankcli-cli](https://github.com/integrallis/rankcli-cli)
216
226
  - **Support**: support@rankcli.dev
217
227
 
218
228
  ## License
@@ -0,0 +1,190 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/wasm-engine.ts
9
+ import * as vm from "vm";
10
+ import * as path from "path";
11
+ import * as fs from "fs";
12
+ import * as os from "os";
13
+ var PREMIUM_CHECK_NAMES = [
14
+ "analyzeFeaturedSnippet",
15
+ "analyzeInternalLinkGraph",
16
+ "analyzeEEATSignals",
17
+ "analyzeContentScience",
18
+ "analyzeKeywordCannibalizationSinglePage",
19
+ "analyzeConversionElements",
20
+ "analyzeKeywordPlacement",
21
+ "analyzeTopicalClusters",
22
+ "analyzePlatformPresence",
23
+ "analyzeInteractiveTools",
24
+ "analyzeFunnelIntent",
25
+ "analyzeNavBoostSignals",
26
+ "analyzeEntitySEO",
27
+ "analyzeFreshnessSignals",
28
+ "analyzeAIContentStructure",
29
+ "analyzeRAGChunkReadiness",
30
+ "analyzeCitationQuality",
31
+ "analyzeAnswerConciseness",
32
+ "analyzeBrandMentionOptimization",
33
+ "analyzeAICitationWorthiness",
34
+ "analyzeReviewEcosystem"
35
+ ];
36
+ var wasmModule = null;
37
+ var auditContext = null;
38
+ async function loadWasm() {
39
+ if (wasmModule) return wasmModule;
40
+ const sea = __require("sea");
41
+ if (sea.isSea()) {
42
+ const gluePath = extractSeaWasmAssets();
43
+ const dynamicRequire = __require("module").createRequire(gluePath);
44
+ wasmModule = dynamicRequire(gluePath);
45
+ return wasmModule;
46
+ }
47
+ const possiblePaths = [
48
+ path.join(__dirname, "../native/pkg/rankcli_audit_engine.js"),
49
+ path.join(__dirname, "../wasm/rankcli_audit_engine.js"),
50
+ path.join(__dirname, "../../native/pkg/rankcli_audit_engine.js")
51
+ ];
52
+ for (const wasmPath of possiblePaths) {
53
+ if (fs.existsSync(wasmPath)) {
54
+ wasmModule = __require(wasmPath);
55
+ return wasmModule;
56
+ }
57
+ }
58
+ throw new Error(
59
+ `WASM engine not found. Run \`npm run build:wasm\` first.
60
+ Searched: ${possiblePaths.join(", ")}`
61
+ );
62
+ }
63
+ function extractSeaWasmAssets() {
64
+ const sea = __require("sea");
65
+ const dir = path.join(os.tmpdir(), "rankcli-wasm-sea");
66
+ fs.mkdirSync(dir, { recursive: true });
67
+ const glueAsset = sea.getRawAsset("rankcli_audit_engine.js");
68
+ const wasmAsset = sea.getRawAsset("rankcli_audit_engine_bg.wasm");
69
+ if (typeof glueAsset === "string" || typeof wasmAsset === "string") {
70
+ throw new Error("SEA assets returned as string, expected binary ArrayBuffer");
71
+ }
72
+ const glueBuffer = Buffer.from(glueAsset);
73
+ const wasmBuffer = Buffer.from(wasmAsset);
74
+ const gluePath = path.join(dir, "rankcli_audit_engine.js");
75
+ const wasmPath = path.join(dir, "rankcli_audit_engine_bg.wasm");
76
+ const existingSize = fs.existsSync(wasmPath) ? fs.statSync(wasmPath).size : -1;
77
+ if (existingSize !== wasmBuffer.length) {
78
+ fs.writeFileSync(gluePath, glueBuffer);
79
+ fs.writeFileSync(wasmPath, wasmBuffer);
80
+ }
81
+ return gluePath;
82
+ }
83
+ async function initEngine(decryptPassphrase) {
84
+ const wasm = await loadWasm();
85
+ try {
86
+ wasm.init_engine(decryptPassphrase);
87
+ } catch (error) {
88
+ throw new Error(`Failed to decrypt premium checks: ${error}`);
89
+ }
90
+ if (!wasm.is_initialized()) {
91
+ throw new Error("Failed to decrypt premium checks: engine did not initialize");
92
+ }
93
+ const jsCode = wasm.get_decrypted_code();
94
+ createExecutionContext(jsCode);
95
+ }
96
+ function createExecutionContext(jsCode) {
97
+ const sandbox = {
98
+ // Console for debugging (can be disabled in production)
99
+ console: {
100
+ log: () => {
101
+ },
102
+ warn: () => {
103
+ },
104
+ error: () => {
105
+ }
106
+ },
107
+ // Timers (some checks may use setTimeout)
108
+ setTimeout,
109
+ clearTimeout,
110
+ // JSON parsing
111
+ JSON,
112
+ // Math operations
113
+ Math,
114
+ // String/Array operations
115
+ String,
116
+ Array,
117
+ Object,
118
+ RegExp,
119
+ Date,
120
+ // URL parsing
121
+ URL,
122
+ URLSearchParams,
123
+ // Node.js globals needed by bundled dependencies (e.g., cheerio)
124
+ Buffer,
125
+ process: { env: {} },
126
+ // TextEncoder/TextDecoder for string handling
127
+ TextEncoder,
128
+ TextDecoder,
129
+ // The RankCLI namespace will be populated by the code
130
+ RankCLI: {},
131
+ // Global reference
132
+ globalThis: {}
133
+ };
134
+ sandbox.globalThis = sandbox;
135
+ auditContext = vm.createContext(sandbox);
136
+ try {
137
+ vm.runInContext(jsCode, auditContext, {
138
+ filename: "rankcli-checks.js",
139
+ timeout: 5e3
140
+ // 5 second timeout for initialization
141
+ });
142
+ } catch (error) {
143
+ throw new Error(`Failed to initialize audit checks: ${error}`);
144
+ }
145
+ }
146
+ function callSandboxCheck(fnName, html, url) {
147
+ if (!auditContext) {
148
+ throw new Error("Engine not initialized. Call initEngine() first.");
149
+ }
150
+ const script = new vm.Script(`
151
+ (function() {
152
+ if (typeof RankCLI.${fnName} !== 'function') {
153
+ throw new Error('RankCLI.${fnName} not found in decrypted code');
154
+ }
155
+ return JSON.stringify(RankCLI.${fnName}(${JSON.stringify(html)}, ${JSON.stringify(url)}));
156
+ })()
157
+ `);
158
+ const resultJson = script.runInContext(auditContext, { timeout: 1e4 });
159
+ return JSON.parse(resultJson);
160
+ }
161
+ async function getPremiumChecks() {
162
+ const wasm = await loadWasm();
163
+ if (!wasm.is_initialized()) {
164
+ throw new Error("Engine not initialized. Call initEngine() first.");
165
+ }
166
+ const checks = {};
167
+ for (const name of PREMIUM_CHECK_NAMES) {
168
+ checks[name] = (html, url) => callSandboxCheck(name, html, url);
169
+ }
170
+ return checks;
171
+ }
172
+ async function getVersion() {
173
+ const wasm = await loadWasm();
174
+ return wasm.version();
175
+ }
176
+ async function isReady() {
177
+ try {
178
+ const wasm = await loadWasm();
179
+ return wasm.is_initialized();
180
+ } catch {
181
+ return false;
182
+ }
183
+ }
184
+
185
+ export {
186
+ initEngine,
187
+ getPremiumChecks,
188
+ getVersion,
189
+ isReady
190
+ };
package/dist/index.js CHANGED
@@ -289,7 +289,7 @@ var import_chalk2 = __toESM(require("chalk"));
289
289
  // src/lib/config.ts
290
290
  var import_conf = __toESM(require("conf"));
291
291
  var import_supabase_js = require("@supabase/supabase-js");
292
- var INJECTED_CONFIG = {"supabaseUrl":"https://bspljbxwbjiqueeyzyat.supabase.co","supabaseAnonKey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJzcGxqYnh3YmppcXVlZXl6eWF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNzQ1NTksImV4cCI6MjA4NDg1MDU1OX0.7gprxM2gT9cZ99sjZURLltrln78dEnwyFFKYpTzBaUU","webAppUrl":"https://rankcli.dev"};
292
+ var INJECTED_CONFIG = {"supabaseUrl":"https://bspljbxwbjiqueeyzyat.supabase.co","supabaseAnonKey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJzcGxqYnh3YmppcXVlZXl6eWF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNzQ1NTksImV4cCI6MjA4NDg1MDU1OX0.7gprxM2gT9cZ99sjZURLltrln78dEnwyFFKYpTzBaUU","webAppUrl":"https://rankcli.dev","decryptPassphrase":"lKFLeIw/xGPa6E8c4XswZX/oiLtJf/yLF/Y6zwovte8="};
293
293
  function getSupabaseUrl() {
294
294
  const url = process.env.RANKCLI_SUPABASE_URL || process.env.VITE_SUPABASE_URL || INJECTED_CONFIG?.supabaseUrl;
295
295
  if (!url) {
@@ -319,6 +319,9 @@ function getSupabaseAnonKeyLazy() {
319
319
  return _supabaseAnonKey;
320
320
  }
321
321
  var WEB_APP_URL = process.env.RANKCLI_WEB_URL || INJECTED_CONFIG?.webAppUrl || "https://rankcli.dev";
322
+ function getDecryptPassphrase() {
323
+ return process.env.RANKCLI_BUILD_KEY || INJECTED_CONFIG?.decryptPassphrase;
324
+ }
322
325
  function getApiUrl() {
323
326
  return process.env.RANKCLI_API_URL || getSupabaseUrlLazy();
324
327
  }
@@ -462,6 +465,7 @@ function getUserInfo() {
462
465
  return {
463
466
  userId: config.get("userId"),
464
467
  email: config.get("email"),
468
+ planId: config.get("planId"),
465
469
  planName: config.get("planName") || "Free",
466
470
  sitesLimit: config.get("sitesLimit") || 1
467
471
  };
@@ -664,8 +668,173 @@ function hasFeature(tier, feature) {
664
668
  return limits?.features?.[feature] ?? false;
665
669
  }
666
670
 
671
+ // src/wasm-engine.ts
672
+ var vm = __toESM(require("vm"));
673
+ var path = __toESM(require("path"));
674
+ var fs = __toESM(require("fs"));
675
+ var os = __toESM(require("os"));
676
+ var PREMIUM_CHECK_NAMES = [
677
+ "analyzeFeaturedSnippet",
678
+ "analyzeInternalLinkGraph",
679
+ "analyzeEEATSignals",
680
+ "analyzeContentScience",
681
+ "analyzeKeywordCannibalizationSinglePage",
682
+ "analyzeConversionElements",
683
+ "analyzeKeywordPlacement",
684
+ "analyzeTopicalClusters",
685
+ "analyzePlatformPresence",
686
+ "analyzeInteractiveTools",
687
+ "analyzeFunnelIntent",
688
+ "analyzeNavBoostSignals",
689
+ "analyzeEntitySEO",
690
+ "analyzeFreshnessSignals",
691
+ "analyzeAIContentStructure",
692
+ "analyzeRAGChunkReadiness",
693
+ "analyzeCitationQuality",
694
+ "analyzeAnswerConciseness",
695
+ "analyzeBrandMentionOptimization",
696
+ "analyzeAICitationWorthiness",
697
+ "analyzeReviewEcosystem"
698
+ ];
699
+ var wasmModule = null;
700
+ var auditContext = null;
701
+ async function loadWasm() {
702
+ if (wasmModule) return wasmModule;
703
+ const sea = require("sea");
704
+ if (sea.isSea()) {
705
+ const gluePath = extractSeaWasmAssets();
706
+ const dynamicRequire = require("module").createRequire(gluePath);
707
+ wasmModule = dynamicRequire(gluePath);
708
+ return wasmModule;
709
+ }
710
+ const possiblePaths = [
711
+ path.join(__dirname, "../native/pkg/rankcli_audit_engine.js"),
712
+ path.join(__dirname, "../wasm/rankcli_audit_engine.js"),
713
+ path.join(__dirname, "../../native/pkg/rankcli_audit_engine.js")
714
+ ];
715
+ for (const wasmPath of possiblePaths) {
716
+ if (fs.existsSync(wasmPath)) {
717
+ wasmModule = require(wasmPath);
718
+ return wasmModule;
719
+ }
720
+ }
721
+ throw new Error(
722
+ `WASM engine not found. Run \`npm run build:wasm\` first.
723
+ Searched: ${possiblePaths.join(", ")}`
724
+ );
725
+ }
726
+ function extractSeaWasmAssets() {
727
+ const sea = require("sea");
728
+ const dir = path.join(os.tmpdir(), "rankcli-wasm-sea");
729
+ fs.mkdirSync(dir, { recursive: true });
730
+ const glueAsset = sea.getRawAsset("rankcli_audit_engine.js");
731
+ const wasmAsset = sea.getRawAsset("rankcli_audit_engine_bg.wasm");
732
+ if (typeof glueAsset === "string" || typeof wasmAsset === "string") {
733
+ throw new Error("SEA assets returned as string, expected binary ArrayBuffer");
734
+ }
735
+ const glueBuffer = Buffer.from(glueAsset);
736
+ const wasmBuffer = Buffer.from(wasmAsset);
737
+ const gluePath = path.join(dir, "rankcli_audit_engine.js");
738
+ const wasmPath = path.join(dir, "rankcli_audit_engine_bg.wasm");
739
+ const existingSize = fs.existsSync(wasmPath) ? fs.statSync(wasmPath).size : -1;
740
+ if (existingSize !== wasmBuffer.length) {
741
+ fs.writeFileSync(gluePath, glueBuffer);
742
+ fs.writeFileSync(wasmPath, wasmBuffer);
743
+ }
744
+ return gluePath;
745
+ }
746
+ async function initEngine(decryptPassphrase) {
747
+ const wasm = await loadWasm();
748
+ try {
749
+ wasm.init_engine(decryptPassphrase);
750
+ } catch (error) {
751
+ throw new Error(`Failed to decrypt premium checks: ${error}`);
752
+ }
753
+ if (!wasm.is_initialized()) {
754
+ throw new Error("Failed to decrypt premium checks: engine did not initialize");
755
+ }
756
+ const jsCode = wasm.get_decrypted_code();
757
+ createExecutionContext(jsCode);
758
+ }
759
+ function createExecutionContext(jsCode) {
760
+ const sandbox = {
761
+ // Console for debugging (can be disabled in production)
762
+ console: {
763
+ log: () => {
764
+ },
765
+ warn: () => {
766
+ },
767
+ error: () => {
768
+ }
769
+ },
770
+ // Timers (some checks may use setTimeout)
771
+ setTimeout,
772
+ clearTimeout,
773
+ // JSON parsing
774
+ JSON,
775
+ // Math operations
776
+ Math,
777
+ // String/Array operations
778
+ String,
779
+ Array,
780
+ Object,
781
+ RegExp,
782
+ Date,
783
+ // URL parsing
784
+ URL,
785
+ URLSearchParams,
786
+ // Node.js globals needed by bundled dependencies (e.g., cheerio)
787
+ Buffer,
788
+ process: { env: {} },
789
+ // TextEncoder/TextDecoder for string handling
790
+ TextEncoder,
791
+ TextDecoder,
792
+ // The RankCLI namespace will be populated by the code
793
+ RankCLI: {},
794
+ // Global reference
795
+ globalThis: {}
796
+ };
797
+ sandbox.globalThis = sandbox;
798
+ auditContext = vm.createContext(sandbox);
799
+ try {
800
+ vm.runInContext(jsCode, auditContext, {
801
+ filename: "rankcli-checks.js",
802
+ timeout: 5e3
803
+ // 5 second timeout for initialization
804
+ });
805
+ } catch (error) {
806
+ throw new Error(`Failed to initialize audit checks: ${error}`);
807
+ }
808
+ }
809
+ function callSandboxCheck(fnName, html, url) {
810
+ if (!auditContext) {
811
+ throw new Error("Engine not initialized. Call initEngine() first.");
812
+ }
813
+ const script = new vm.Script(`
814
+ (function() {
815
+ if (typeof RankCLI.${fnName} !== 'function') {
816
+ throw new Error('RankCLI.${fnName} not found in decrypted code');
817
+ }
818
+ return JSON.stringify(RankCLI.${fnName}(${JSON.stringify(html)}, ${JSON.stringify(url)}));
819
+ })()
820
+ `);
821
+ const resultJson = script.runInContext(auditContext, { timeout: 1e4 });
822
+ return JSON.parse(resultJson);
823
+ }
824
+ async function getPremiumChecks() {
825
+ const wasm = await loadWasm();
826
+ if (!wasm.is_initialized()) {
827
+ throw new Error("Engine not initialized. Call initEngine() first.");
828
+ }
829
+ const checks = {};
830
+ for (const name of PREMIUM_CHECK_NAMES) {
831
+ checks[name] = (html, url) => callSandboxCheck(name, html, url);
832
+ }
833
+ return checks;
834
+ }
835
+
667
836
  // src/commands/audit.ts
668
- var { detectFramework: detectFramework2, getFrameworkDisplayName, getFrameworkIssues } = import_agent_runtime.frameworks;
837
+ var { detectFramework: detectFramework2, getFrameworkDisplayName } = import_agent_runtime.frameworks;
669
838
  async function fetchHtml(url, timeout = 1e4) {
670
839
  try {
671
840
  const controller = new AbortController();
@@ -868,6 +1037,21 @@ async function audit(options) {
868
1037
  const aiProvider = options.aiProvider || "openai";
869
1038
  const aiKey = options.aiKey || (aiProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY : process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY) || "";
870
1039
  const loggedIn = isLoggedIn();
1040
+ const { planId } = getUserInfo();
1041
+ const tierId = planId && planId in TIER_LIMITS ? planId : loggedIn ? "free" : "free_anon";
1042
+ const checksLimit = TIER_LIMITS[tierId].checks;
1043
+ let premiumChecks;
1044
+ if (checksLimit > TIER_LIMITS.free.checks) {
1045
+ const passphrase = getDecryptPassphrase();
1046
+ if (passphrase) {
1047
+ try {
1048
+ await initEngine(passphrase);
1049
+ premiumChecks = await getPremiumChecks();
1050
+ } catch (error) {
1051
+ console.error(import_chalk2.default.dim(`(premium checks unavailable: ${error instanceof Error ? error.message : error})`));
1052
+ }
1053
+ }
1054
+ }
871
1055
  if (!url) {
872
1056
  console.log("\n\u26A0\uFE0F Please provide a URL to audit:");
873
1057
  console.log(" rankcli audit --url https://your-site.com");
@@ -912,7 +1096,14 @@ async function audit(options) {
912
1096
  const report = await (0, import_agent_runtime.runFullAudit)({
913
1097
  url: pageUrl,
914
1098
  maxPages: 1,
915
- checkBrokenLinks: options.checkLinks || false
1099
+ checkBrokenLinks: options.checkLinks || false,
1100
+ // Bundled with --check-links: both mean "verify actual link
1101
+ // targets, which costs real requests" - hreflang reciprocity and
1102
+ // broken-target checks were previously unreachable from any real
1103
+ // caller since nothing ever set this true.
1104
+ checkHreflangUrls: options.checkLinks || false,
1105
+ checksLimit,
1106
+ premiumChecks
916
1107
  });
917
1108
  auditSpinner.text = `Auditing pages... (${index + 1}/${pagesToAudit.length} complete)`;
918
1109
  return { report, url: pageUrl };
@@ -943,16 +1134,7 @@ async function audit(options) {
943
1134
  return;
944
1135
  }
945
1136
  auditSpinner.succeed(`Audited ${successCount}/${pagesToAudit.length} pages in ${parallelTime}ms`);
946
- let mergedIssues = mergeIssues(allIssues);
947
- if (detectedFramework.framework !== "unknown" && homepageHtml) {
948
- try {
949
- const frameworkIssues = await getFrameworkIssues(detectedFramework, homepageHtml, url);
950
- if (frameworkIssues.length > 0) {
951
- mergedIssues = [...mergedIssues, ...frameworkIssues];
952
- }
953
- } catch {
954
- }
955
- }
1137
+ const mergedIssues = mergeIssues(allIssues);
956
1138
  let aiAnalysis = null;
957
1139
  if (enableAI && aiKey) {
958
1140
  const aiSpinner = (0, import_ora.default)(`Running AI analysis (${aiProvider})...`).start();
@@ -2673,13 +2855,13 @@ function extractBodyText(html) {
2673
2855
 
2674
2856
  // src/commands/setup.ts
2675
2857
  var readline2 = __toESM(require("readline"));
2676
- var fs = __toESM(require("fs"));
2677
- var path = __toESM(require("path"));
2858
+ var fs2 = __toESM(require("fs"));
2859
+ var path2 = __toESM(require("path"));
2678
2860
  var import_agent_runtime8 = require("@rankcli/agent-runtime");
2679
2861
  async function setup(options) {
2680
2862
  console.log("\n\u{1F527} SEO Autopilot - Setup\n");
2681
2863
  const projectPath = process.cwd();
2682
- if (!fs.existsSync(path.join(projectPath, "package.json")) && !fs.existsSync(path.join(projectPath, "index.html"))) {
2864
+ if (!fs2.existsSync(path2.join(projectPath, "package.json")) && !fs2.existsSync(path2.join(projectPath, "index.html"))) {
2683
2865
  console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
2684
2866
  }
2685
2867
  if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
@@ -2790,16 +2972,16 @@ async function setupGA4(projectPath, measurementId) {
2790
2972
  console.log(result.code);
2791
2973
  }
2792
2974
  }
2793
- const envExamplePath = path.join(projectPath, ".env.example");
2975
+ const envExamplePath = path2.join(projectPath, ".env.example");
2794
2976
  const envContent = (0, import_agent_runtime8.generateGA4EnvTemplate)();
2795
- if (fs.existsSync(envExamplePath)) {
2796
- const existing = fs.readFileSync(envExamplePath, "utf-8");
2977
+ if (fs2.existsSync(envExamplePath)) {
2978
+ const existing = fs2.readFileSync(envExamplePath, "utf-8");
2797
2979
  if (!existing.includes("GA4_MEASUREMENT_ID")) {
2798
- fs.appendFileSync(envExamplePath, "\n" + envContent);
2980
+ fs2.appendFileSync(envExamplePath, "\n" + envContent);
2799
2981
  console.log(" Updated: .env.example");
2800
2982
  }
2801
2983
  } else {
2802
- fs.writeFileSync(envExamplePath, envContent);
2984
+ fs2.writeFileSync(envExamplePath, envContent);
2803
2985
  console.log(" Created: .env.example");
2804
2986
  }
2805
2987
  console.log("");
@@ -2833,7 +3015,7 @@ async function setupGitHubAction(projectPath, schedule, siteUrl) {
2833
3015
  const { files, instructions } = (0, import_agent_runtime8.writeGitHubActionFiles)(projectPath, config2);
2834
3016
  console.log("\u2705 GitHub Action files created:");
2835
3017
  for (const file of files) {
2836
- console.log(` - ${path.relative(projectPath, file)}`);
3018
+ console.log(` - ${path2.relative(projectPath, file)}`);
2837
3019
  }
2838
3020
  console.log(instructions);
2839
3021
  }
@@ -3224,7 +3406,7 @@ async function fetchAndSaveSubscription() {
3224
3406
  }
3225
3407
 
3226
3408
  // src/version.ts
3227
- var VERSION = "0.0.21";
3409
+ var VERSION = "0.0.23";
3228
3410
 
3229
3411
  // src/index.ts
3230
3412
  var program = new import_commander.Command();
package/dist/index.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-Y6FXYEAI.mjs";
2
+ import {
3
+ getPremiumChecks,
4
+ initEngine
5
+ } from "./chunk-NF755NKY.mjs";
3
6
 
4
7
  // src/index.ts
5
8
  import { Command } from "commander";
@@ -267,7 +270,7 @@ import chalk2 from "chalk";
267
270
  // src/lib/config.ts
268
271
  import Conf from "conf";
269
272
  import { createClient } from "@supabase/supabase-js";
270
- const INJECTED_CONFIG = {"supabaseUrl":"https://bspljbxwbjiqueeyzyat.supabase.co","supabaseAnonKey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJzcGxqYnh3YmppcXVlZXl6eWF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNzQ1NTksImV4cCI6MjA4NDg1MDU1OX0.7gprxM2gT9cZ99sjZURLltrln78dEnwyFFKYpTzBaUU","webAppUrl":"https://rankcli.dev"};
273
+ const INJECTED_CONFIG = {"supabaseUrl":"https://bspljbxwbjiqueeyzyat.supabase.co","supabaseAnonKey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJzcGxqYnh3YmppcXVlZXl6eWF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNzQ1NTksImV4cCI6MjA4NDg1MDU1OX0.7gprxM2gT9cZ99sjZURLltrln78dEnwyFFKYpTzBaUU","webAppUrl":"https://rankcli.dev","decryptPassphrase":"lKFLeIw/xGPa6E8c4XswZX/oiLtJf/yLF/Y6zwovte8="};
271
274
  function getSupabaseUrl() {
272
275
  const url = process.env.RANKCLI_SUPABASE_URL || process.env.VITE_SUPABASE_URL || INJECTED_CONFIG?.supabaseUrl;
273
276
  if (!url) {
@@ -297,6 +300,9 @@ function getSupabaseAnonKeyLazy() {
297
300
  return _supabaseAnonKey;
298
301
  }
299
302
  var WEB_APP_URL = process.env.RANKCLI_WEB_URL || INJECTED_CONFIG?.webAppUrl || "https://rankcli.dev";
303
+ function getDecryptPassphrase() {
304
+ return process.env.RANKCLI_BUILD_KEY || INJECTED_CONFIG?.decryptPassphrase;
305
+ }
300
306
  function getApiUrl() {
301
307
  return process.env.RANKCLI_API_URL || getSupabaseUrlLazy();
302
308
  }
@@ -440,6 +446,7 @@ function getUserInfo() {
440
446
  return {
441
447
  userId: config.get("userId"),
442
448
  email: config.get("email"),
449
+ planId: config.get("planId"),
443
450
  planName: config.get("planName") || "Free",
444
451
  sitesLimit: config.get("sitesLimit") || 1
445
452
  };
@@ -643,7 +650,7 @@ function hasFeature(tier, feature) {
643
650
  }
644
651
 
645
652
  // src/commands/audit.ts
646
- var { detectFramework: detectFramework2, getFrameworkDisplayName, getFrameworkIssues } = frameworks;
653
+ var { detectFramework: detectFramework2, getFrameworkDisplayName } = frameworks;
647
654
  async function fetchHtml(url, timeout = 1e4) {
648
655
  try {
649
656
  const controller = new AbortController();
@@ -846,6 +853,21 @@ async function audit(options) {
846
853
  const aiProvider = options.aiProvider || "openai";
847
854
  const aiKey = options.aiKey || (aiProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY : process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY) || "";
848
855
  const loggedIn = isLoggedIn();
856
+ const { planId } = getUserInfo();
857
+ const tierId = planId && planId in TIER_LIMITS ? planId : loggedIn ? "free" : "free_anon";
858
+ const checksLimit = TIER_LIMITS[tierId].checks;
859
+ let premiumChecks;
860
+ if (checksLimit > TIER_LIMITS.free.checks) {
861
+ const passphrase = getDecryptPassphrase();
862
+ if (passphrase) {
863
+ try {
864
+ await initEngine(passphrase);
865
+ premiumChecks = await getPremiumChecks();
866
+ } catch (error) {
867
+ console.error(chalk2.dim(`(premium checks unavailable: ${error instanceof Error ? error.message : error})`));
868
+ }
869
+ }
870
+ }
849
871
  if (!url) {
850
872
  console.log("\n\u26A0\uFE0F Please provide a URL to audit:");
851
873
  console.log(" rankcli audit --url https://your-site.com");
@@ -890,7 +912,14 @@ async function audit(options) {
890
912
  const report = await runFullAudit({
891
913
  url: pageUrl,
892
914
  maxPages: 1,
893
- checkBrokenLinks: options.checkLinks || false
915
+ checkBrokenLinks: options.checkLinks || false,
916
+ // Bundled with --check-links: both mean "verify actual link
917
+ // targets, which costs real requests" - hreflang reciprocity and
918
+ // broken-target checks were previously unreachable from any real
919
+ // caller since nothing ever set this true.
920
+ checkHreflangUrls: options.checkLinks || false,
921
+ checksLimit,
922
+ premiumChecks
894
923
  });
895
924
  auditSpinner.text = `Auditing pages... (${index + 1}/${pagesToAudit.length} complete)`;
896
925
  return { report, url: pageUrl };
@@ -921,16 +950,7 @@ async function audit(options) {
921
950
  return;
922
951
  }
923
952
  auditSpinner.succeed(`Audited ${successCount}/${pagesToAudit.length} pages in ${parallelTime}ms`);
924
- let mergedIssues = mergeIssues(allIssues);
925
- if (detectedFramework.framework !== "unknown" && homepageHtml) {
926
- try {
927
- const frameworkIssues = await getFrameworkIssues(detectedFramework, homepageHtml, url);
928
- if (frameworkIssues.length > 0) {
929
- mergedIssues = [...mergedIssues, ...frameworkIssues];
930
- }
931
- } catch {
932
- }
933
- }
953
+ const mergedIssues = mergeIssues(allIssues);
934
954
  let aiAnalysis = null;
935
955
  if (enableAI && aiKey) {
936
956
  const aiSpinner = ora(`Running AI analysis (${aiProvider})...`).start();
@@ -1615,7 +1635,7 @@ ${chalk3.green("\u2705")} Changes committed`);
1615
1635
  import ora3 from "ora";
1616
1636
  import chalk4 from "chalk";
1617
1637
  import { table } from "table";
1618
- import { RankTracker, TIER_LIMITS as TIER_LIMITS3 } from "@rankcli/agent-runtime";
1638
+ import { RankTracker, TIER_LIMITS as TIER_LIMITS2 } from "@rankcli/agent-runtime";
1619
1639
  async function getProjectForUrl(url) {
1620
1640
  const supabase = await getAuthenticatedClient();
1621
1641
  if (!supabase) return null;
@@ -1645,7 +1665,7 @@ async function getProjectForUrl(url) {
1645
1665
  };
1646
1666
  }
1647
1667
  function getTierLimits(tier) {
1648
- const limits = TIER_LIMITS3[tier] || TIER_LIMITS3.free;
1668
+ const limits = TIER_LIMITS2[tier] || TIER_LIMITS2.free;
1649
1669
  return {
1650
1670
  maxKeywords: limits.maxKeywords,
1651
1671
  checksPerDay: limits.checksPerDay
@@ -3241,7 +3261,7 @@ async function fetchAndSaveSubscription() {
3241
3261
  }
3242
3262
 
3243
3263
  // src/version.ts
3244
- var VERSION = "0.0.21";
3264
+ var VERSION = "0.0.23";
3245
3265
 
3246
3266
  // src/index.ts
3247
3267
  var program = new Command();
@@ -1,55 +1,43 @@
1
+ import { AuditIssue } from '@rankcli/agent-runtime';
2
+
1
3
  /**
2
4
  * WASM Engine Wrapper
3
5
  *
4
- * This module bridges the WASM binary (which contains encrypted checks)
5
- * with Node.js execution. The flow is:
6
+ * Bridges the WASM binary (which contains AES-encrypted paid-tier check
7
+ * source) with Node.js execution:
6
8
  *
7
- * 1. WASM decrypts the JS code using the license key
9
+ * 1. WASM decrypts the JS code using a fixed, build-embedded passphrase
8
10
  * 2. We get the decrypted code from WASM
9
11
  * 3. We execute it in a Node.js VM sandbox
10
- * 4. Results are returned to the caller
12
+ * 4. Individual premium check functions are called from that sandbox
11
13
  *
12
- * Security: The JS source code only exists in memory briefly during execution.
13
- * Users cannot extract it from node_modules - they only see the WASM binary.
14
+ * This is code protection, not access control: entitlement (is this user
15
+ * actually paid?) is decided by the caller, from the real Supabase-backed
16
+ * apiKey/plan the CLI validates on login - see commands/audit.ts. This
17
+ * module should only ever be initialized after that check passes; there
18
+ * is no per-user license key, and passing an arbitrary string here does
19
+ * not grant anything a validated apiKey didn't already grant.
14
20
  */
15
- interface AuditResult {
16
- success: boolean;
17
- score: number;
21
+
22
+ type PremiumCheckFn = (html: string, url: string) => {
18
23
  issues: AuditIssue[];
19
- checksRun: number;
20
- tier: string;
21
- }
22
- interface AuditIssue {
23
- code: string;
24
- severity: 'error' | 'warning' | 'notice';
25
- category: string;
26
- title: string;
27
- description?: string;
28
- impact?: string;
29
- howToFix?: string;
30
- affectedUrls?: string[];
31
- }
32
- type Tier = 'free' | 'registered' | 'solo' | 'pro' | 'agency';
33
- /**
34
- * Initialize the engine with a license key
35
- */
36
- declare function initEngine(licenseKey?: string): Promise<void>;
37
- /**
38
- * Run an SEO audit on a URL
39
- */
40
- declare function runAudit(url: string, html: string): Promise<AuditResult>;
41
- /**
42
- * Get the current tier
43
- */
44
- declare function getTier(): Promise<Tier>;
24
+ data: unknown;
25
+ };
26
+ declare const PREMIUM_CHECK_NAMES: readonly ["analyzeFeaturedSnippet", "analyzeInternalLinkGraph", "analyzeEEATSignals", "analyzeContentScience", "analyzeKeywordCannibalizationSinglePage", "analyzeConversionElements", "analyzeKeywordPlacement", "analyzeTopicalClusters", "analyzePlatformPresence", "analyzeInteractiveTools", "analyzeFunnelIntent", "analyzeNavBoostSignals", "analyzeEntitySEO", "analyzeFreshnessSignals", "analyzeAIContentStructure", "analyzeRAGChunkReadiness", "analyzeCitationQuality", "analyzeAnswerConciseness", "analyzeBrandMentionOptimization", "analyzeAICitationWorthiness", "analyzeReviewEcosystem"];
27
+ type PremiumChecks = Record<(typeof PREMIUM_CHECK_NAMES)[number], PremiumCheckFn>;
45
28
  /**
46
- * Get the checks limit for the current tier
29
+ * Decrypt the embedded premium checks and prepare them for execution.
30
+ * Only call this once the caller has independently confirmed the user is
31
+ * entitled to premium checks (a validated paid apiKey) - see the module
32
+ * doc comment above.
47
33
  */
48
- declare function getChecksLimit(): Promise<number>;
34
+ declare function initEngine(decryptPassphrase: string): Promise<void>;
49
35
  /**
50
- * Validate a license key without initializing
36
+ * Build a PremiumChecks object (matching agent-runtime's
37
+ * premium-checks.types.ts) whose methods each call into the decrypted
38
+ * VM sandbox. Pass the result as runFullAudit({ premiumChecks }).
51
39
  */
52
- declare function validateLicense(licenseKey: string): Promise<Tier>;
40
+ declare function getPremiumChecks(): Promise<PremiumChecks>;
53
41
  /**
54
42
  * Get engine version info
55
43
  */
@@ -59,4 +47,4 @@ declare function getVersion(): Promise<string>;
59
47
  */
60
48
  declare function isReady(): Promise<boolean>;
61
49
 
62
- export { type AuditIssue, type AuditResult, type Tier, getChecksLimit, getTier, getVersion, initEngine, isReady, runAudit, validateLicense };
50
+ export { type PremiumCheckFn, type PremiumChecks, getPremiumChecks, getVersion, initEngine, isReady };
@@ -1,55 +1,43 @@
1
+ import { AuditIssue } from '@rankcli/agent-runtime';
2
+
1
3
  /**
2
4
  * WASM Engine Wrapper
3
5
  *
4
- * This module bridges the WASM binary (which contains encrypted checks)
5
- * with Node.js execution. The flow is:
6
+ * Bridges the WASM binary (which contains AES-encrypted paid-tier check
7
+ * source) with Node.js execution:
6
8
  *
7
- * 1. WASM decrypts the JS code using the license key
9
+ * 1. WASM decrypts the JS code using a fixed, build-embedded passphrase
8
10
  * 2. We get the decrypted code from WASM
9
11
  * 3. We execute it in a Node.js VM sandbox
10
- * 4. Results are returned to the caller
12
+ * 4. Individual premium check functions are called from that sandbox
11
13
  *
12
- * Security: The JS source code only exists in memory briefly during execution.
13
- * Users cannot extract it from node_modules - they only see the WASM binary.
14
+ * This is code protection, not access control: entitlement (is this user
15
+ * actually paid?) is decided by the caller, from the real Supabase-backed
16
+ * apiKey/plan the CLI validates on login - see commands/audit.ts. This
17
+ * module should only ever be initialized after that check passes; there
18
+ * is no per-user license key, and passing an arbitrary string here does
19
+ * not grant anything a validated apiKey didn't already grant.
14
20
  */
15
- interface AuditResult {
16
- success: boolean;
17
- score: number;
21
+
22
+ type PremiumCheckFn = (html: string, url: string) => {
18
23
  issues: AuditIssue[];
19
- checksRun: number;
20
- tier: string;
21
- }
22
- interface AuditIssue {
23
- code: string;
24
- severity: 'error' | 'warning' | 'notice';
25
- category: string;
26
- title: string;
27
- description?: string;
28
- impact?: string;
29
- howToFix?: string;
30
- affectedUrls?: string[];
31
- }
32
- type Tier = 'free' | 'registered' | 'solo' | 'pro' | 'agency';
33
- /**
34
- * Initialize the engine with a license key
35
- */
36
- declare function initEngine(licenseKey?: string): Promise<void>;
37
- /**
38
- * Run an SEO audit on a URL
39
- */
40
- declare function runAudit(url: string, html: string): Promise<AuditResult>;
41
- /**
42
- * Get the current tier
43
- */
44
- declare function getTier(): Promise<Tier>;
24
+ data: unknown;
25
+ };
26
+ declare const PREMIUM_CHECK_NAMES: readonly ["analyzeFeaturedSnippet", "analyzeInternalLinkGraph", "analyzeEEATSignals", "analyzeContentScience", "analyzeKeywordCannibalizationSinglePage", "analyzeConversionElements", "analyzeKeywordPlacement", "analyzeTopicalClusters", "analyzePlatformPresence", "analyzeInteractiveTools", "analyzeFunnelIntent", "analyzeNavBoostSignals", "analyzeEntitySEO", "analyzeFreshnessSignals", "analyzeAIContentStructure", "analyzeRAGChunkReadiness", "analyzeCitationQuality", "analyzeAnswerConciseness", "analyzeBrandMentionOptimization", "analyzeAICitationWorthiness", "analyzeReviewEcosystem"];
27
+ type PremiumChecks = Record<(typeof PREMIUM_CHECK_NAMES)[number], PremiumCheckFn>;
45
28
  /**
46
- * Get the checks limit for the current tier
29
+ * Decrypt the embedded premium checks and prepare them for execution.
30
+ * Only call this once the caller has independently confirmed the user is
31
+ * entitled to premium checks (a validated paid apiKey) - see the module
32
+ * doc comment above.
47
33
  */
48
- declare function getChecksLimit(): Promise<number>;
34
+ declare function initEngine(decryptPassphrase: string): Promise<void>;
49
35
  /**
50
- * Validate a license key without initializing
36
+ * Build a PremiumChecks object (matching agent-runtime's
37
+ * premium-checks.types.ts) whose methods each call into the decrypted
38
+ * VM sandbox. Pass the result as runFullAudit({ premiumChecks }).
51
39
  */
52
- declare function validateLicense(licenseKey: string): Promise<Tier>;
40
+ declare function getPremiumChecks(): Promise<PremiumChecks>;
53
41
  /**
54
42
  * Get engine version info
55
43
  */
@@ -59,4 +47,4 @@ declare function getVersion(): Promise<string>;
59
47
  */
60
48
  declare function isReady(): Promise<boolean>;
61
49
 
62
- export { type AuditIssue, type AuditResult, type Tier, getChecksLimit, getTier, getVersion, initEngine, isReady, runAudit, validateLicense };
50
+ export { type PremiumCheckFn, type PremiumChecks, getPremiumChecks, getVersion, initEngine, isReady };
@@ -30,22 +30,50 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/wasm-engine.ts
31
31
  var wasm_engine_exports = {};
32
32
  __export(wasm_engine_exports, {
33
- getChecksLimit: () => getChecksLimit,
34
- getTier: () => getTier,
33
+ getPremiumChecks: () => getPremiumChecks,
35
34
  getVersion: () => getVersion,
36
35
  initEngine: () => initEngine,
37
- isReady: () => isReady,
38
- runAudit: () => runAudit,
39
- validateLicense: () => validateLicense
36
+ isReady: () => isReady
40
37
  });
41
38
  module.exports = __toCommonJS(wasm_engine_exports);
42
39
  var vm = __toESM(require("vm"));
43
40
  var path = __toESM(require("path"));
44
41
  var fs = __toESM(require("fs"));
42
+ var os = __toESM(require("os"));
43
+ var PREMIUM_CHECK_NAMES = [
44
+ "analyzeFeaturedSnippet",
45
+ "analyzeInternalLinkGraph",
46
+ "analyzeEEATSignals",
47
+ "analyzeContentScience",
48
+ "analyzeKeywordCannibalizationSinglePage",
49
+ "analyzeConversionElements",
50
+ "analyzeKeywordPlacement",
51
+ "analyzeTopicalClusters",
52
+ "analyzePlatformPresence",
53
+ "analyzeInteractiveTools",
54
+ "analyzeFunnelIntent",
55
+ "analyzeNavBoostSignals",
56
+ "analyzeEntitySEO",
57
+ "analyzeFreshnessSignals",
58
+ "analyzeAIContentStructure",
59
+ "analyzeRAGChunkReadiness",
60
+ "analyzeCitationQuality",
61
+ "analyzeAnswerConciseness",
62
+ "analyzeBrandMentionOptimization",
63
+ "analyzeAICitationWorthiness",
64
+ "analyzeReviewEcosystem"
65
+ ];
45
66
  var wasmModule = null;
46
67
  var auditContext = null;
47
68
  async function loadWasm() {
48
69
  if (wasmModule) return wasmModule;
70
+ const sea = require("sea");
71
+ if (sea.isSea()) {
72
+ const gluePath = extractSeaWasmAssets();
73
+ const dynamicRequire = require("module").createRequire(gluePath);
74
+ wasmModule = dynamicRequire(gluePath);
75
+ return wasmModule;
76
+ }
49
77
  const possiblePaths = [
50
78
  path.join(__dirname, "../native/pkg/rankcli_audit_engine.js"),
51
79
  path.join(__dirname, "../wasm/rankcli_audit_engine.js"),
@@ -62,16 +90,35 @@ async function loadWasm() {
62
90
  Searched: ${possiblePaths.join(", ")}`
63
91
  );
64
92
  }
65
- async function initEngine(licenseKey) {
93
+ function extractSeaWasmAssets() {
94
+ const sea = require("sea");
95
+ const dir = path.join(os.tmpdir(), "rankcli-wasm-sea");
96
+ fs.mkdirSync(dir, { recursive: true });
97
+ const glueAsset = sea.getRawAsset("rankcli_audit_engine.js");
98
+ const wasmAsset = sea.getRawAsset("rankcli_audit_engine_bg.wasm");
99
+ if (typeof glueAsset === "string" || typeof wasmAsset === "string") {
100
+ throw new Error("SEA assets returned as string, expected binary ArrayBuffer");
101
+ }
102
+ const glueBuffer = Buffer.from(glueAsset);
103
+ const wasmBuffer = Buffer.from(wasmAsset);
104
+ const gluePath = path.join(dir, "rankcli_audit_engine.js");
105
+ const wasmPath = path.join(dir, "rankcli_audit_engine_bg.wasm");
106
+ const existingSize = fs.existsSync(wasmPath) ? fs.statSync(wasmPath).size : -1;
107
+ if (existingSize !== wasmBuffer.length) {
108
+ fs.writeFileSync(gluePath, glueBuffer);
109
+ fs.writeFileSync(wasmPath, wasmBuffer);
110
+ }
111
+ return gluePath;
112
+ }
113
+ async function initEngine(decryptPassphrase) {
66
114
  const wasm = await loadWasm();
67
- const key = licenseKey || process.env.RANKCLI_LICENSE_KEY || "";
68
115
  try {
69
- wasm.init_engine(key);
116
+ wasm.init_engine(decryptPassphrase);
70
117
  } catch (error) {
71
- if (error instanceof Error && error.message.includes("Invalid license")) {
72
- throw new Error("Invalid license key. Get one at https://rankcli.dev/pricing");
73
- }
74
- throw error;
118
+ throw new Error(`Failed to decrypt premium checks: ${error}`);
119
+ }
120
+ if (!wasm.is_initialized()) {
121
+ throw new Error("Failed to decrypt premium checks: engine did not initialize");
75
122
  }
76
123
  const jsCode = wasm.get_decrypted_code();
77
124
  createExecutionContext(jsCode);
@@ -126,59 +173,31 @@ function createExecutionContext(jsCode) {
126
173
  throw new Error(`Failed to initialize audit checks: ${error}`);
127
174
  }
128
175
  }
129
- async function runAudit(url, html) {
176
+ function callSandboxCheck(fnName, html, url) {
177
+ if (!auditContext) {
178
+ throw new Error("Engine not initialized. Call initEngine() first.");
179
+ }
180
+ const script = new vm.Script(`
181
+ (function() {
182
+ if (typeof RankCLI.${fnName} !== 'function') {
183
+ throw new Error('RankCLI.${fnName} not found in decrypted code');
184
+ }
185
+ return JSON.stringify(RankCLI.${fnName}(${JSON.stringify(html)}, ${JSON.stringify(url)}));
186
+ })()
187
+ `);
188
+ const resultJson = script.runInContext(auditContext, { timeout: 1e4 });
189
+ return JSON.parse(resultJson);
190
+ }
191
+ async function getPremiumChecks() {
130
192
  const wasm = await loadWasm();
131
193
  if (!wasm.is_initialized()) {
132
194
  throw new Error("Engine not initialized. Call initEngine() first.");
133
195
  }
134
- if (!auditContext) {
135
- throw new Error("Execution context not ready");
136
- }
137
- const checksLimit = wasm.get_checks_limit();
138
- const tier = wasm.get_tier();
139
- try {
140
- const script = new vm.Script(`
141
- (function() {
142
- const url = ${JSON.stringify(url)};
143
- const html = ${JSON.stringify(html)};
144
- const checksLimit = ${checksLimit};
145
-
146
- if (typeof RankCLI.runAudit === 'function') {
147
- return JSON.stringify(RankCLI.runAudit(url, html, checksLimit));
148
- } else if (typeof RankCLI.runFullAudit === 'function') {
149
- // Synchronous wrapper for async function
150
- return JSON.stringify({
151
- success: true,
152
- score: 85,
153
- issues: [],
154
- checksRun: checksLimit,
155
- tier: "${tier}"
156
- });
157
- } else {
158
- throw new Error('RankCLI.runAudit not found in decrypted code');
159
- }
160
- })()
161
- `);
162
- const resultJson = script.runInContext(auditContext, {
163
- timeout: 3e4
164
- // 30 second timeout for audit
165
- });
166
- return JSON.parse(resultJson);
167
- } catch (error) {
168
- throw new Error(`Audit execution failed: ${error}`);
196
+ const checks = {};
197
+ for (const name of PREMIUM_CHECK_NAMES) {
198
+ checks[name] = (html, url) => callSandboxCheck(name, html, url);
169
199
  }
170
- }
171
- async function getTier() {
172
- const wasm = await loadWasm();
173
- return wasm.get_tier();
174
- }
175
- async function getChecksLimit() {
176
- const wasm = await loadWasm();
177
- return wasm.get_checks_limit();
178
- }
179
- async function validateLicense(licenseKey) {
180
- const wasm = await loadWasm();
181
- return wasm.validate_license(licenseKey);
200
+ return checks;
182
201
  }
183
202
  async function getVersion() {
184
203
  const wasm = await loadWasm();
@@ -194,11 +213,8 @@ async function isReady() {
194
213
  }
195
214
  // Annotate the CommonJS export names for ESM import in node:
196
215
  0 && (module.exports = {
197
- getChecksLimit,
198
- getTier,
216
+ getPremiumChecks,
199
217
  getVersion,
200
218
  initEngine,
201
- isReady,
202
- runAudit,
203
- validateLicense
219
+ isReady
204
220
  });
@@ -1,167 +1,12 @@
1
1
  import {
2
- __require
3
- } from "./chunk-Y6FXYEAI.mjs";
4
-
5
- // src/wasm-engine.ts
6
- import * as vm from "vm";
7
- import * as path from "path";
8
- import * as fs from "fs";
9
- var wasmModule = null;
10
- var auditContext = null;
11
- async function loadWasm() {
12
- if (wasmModule) return wasmModule;
13
- const possiblePaths = [
14
- path.join(__dirname, "../native/pkg/rankcli_audit_engine.js"),
15
- path.join(__dirname, "../wasm/rankcli_audit_engine.js"),
16
- path.join(__dirname, "../../native/pkg/rankcli_audit_engine.js")
17
- ];
18
- for (const wasmPath of possiblePaths) {
19
- if (fs.existsSync(wasmPath)) {
20
- wasmModule = __require(wasmPath);
21
- return wasmModule;
22
- }
23
- }
24
- throw new Error(
25
- `WASM engine not found. Run \`npm run build:wasm\` first.
26
- Searched: ${possiblePaths.join(", ")}`
27
- );
28
- }
29
- async function initEngine(licenseKey) {
30
- const wasm = await loadWasm();
31
- const key = licenseKey || process.env.RANKCLI_LICENSE_KEY || "";
32
- try {
33
- wasm.init_engine(key);
34
- } catch (error) {
35
- if (error instanceof Error && error.message.includes("Invalid license")) {
36
- throw new Error("Invalid license key. Get one at https://rankcli.dev/pricing");
37
- }
38
- throw error;
39
- }
40
- const jsCode = wasm.get_decrypted_code();
41
- createExecutionContext(jsCode);
42
- }
43
- function createExecutionContext(jsCode) {
44
- const sandbox = {
45
- // Console for debugging (can be disabled in production)
46
- console: {
47
- log: () => {
48
- },
49
- warn: () => {
50
- },
51
- error: () => {
52
- }
53
- },
54
- // Timers (some checks may use setTimeout)
55
- setTimeout,
56
- clearTimeout,
57
- // JSON parsing
58
- JSON,
59
- // Math operations
60
- Math,
61
- // String/Array operations
62
- String,
63
- Array,
64
- Object,
65
- RegExp,
66
- Date,
67
- // URL parsing
68
- URL,
69
- URLSearchParams,
70
- // Node.js globals needed by bundled dependencies (e.g., cheerio)
71
- Buffer,
72
- process: { env: {} },
73
- // TextEncoder/TextDecoder for string handling
74
- TextEncoder,
75
- TextDecoder,
76
- // The RankCLI namespace will be populated by the code
77
- RankCLI: {},
78
- // Global reference
79
- globalThis: {}
80
- };
81
- sandbox.globalThis = sandbox;
82
- auditContext = vm.createContext(sandbox);
83
- try {
84
- vm.runInContext(jsCode, auditContext, {
85
- filename: "rankcli-checks.js",
86
- timeout: 5e3
87
- // 5 second timeout for initialization
88
- });
89
- } catch (error) {
90
- throw new Error(`Failed to initialize audit checks: ${error}`);
91
- }
92
- }
93
- async function runAudit(url, html) {
94
- const wasm = await loadWasm();
95
- if (!wasm.is_initialized()) {
96
- throw new Error("Engine not initialized. Call initEngine() first.");
97
- }
98
- if (!auditContext) {
99
- throw new Error("Execution context not ready");
100
- }
101
- const checksLimit = wasm.get_checks_limit();
102
- const tier = wasm.get_tier();
103
- try {
104
- const script = new vm.Script(`
105
- (function() {
106
- const url = ${JSON.stringify(url)};
107
- const html = ${JSON.stringify(html)};
108
- const checksLimit = ${checksLimit};
109
-
110
- if (typeof RankCLI.runAudit === 'function') {
111
- return JSON.stringify(RankCLI.runAudit(url, html, checksLimit));
112
- } else if (typeof RankCLI.runFullAudit === 'function') {
113
- // Synchronous wrapper for async function
114
- return JSON.stringify({
115
- success: true,
116
- score: 85,
117
- issues: [],
118
- checksRun: checksLimit,
119
- tier: "${tier}"
120
- });
121
- } else {
122
- throw new Error('RankCLI.runAudit not found in decrypted code');
123
- }
124
- })()
125
- `);
126
- const resultJson = script.runInContext(auditContext, {
127
- timeout: 3e4
128
- // 30 second timeout for audit
129
- });
130
- return JSON.parse(resultJson);
131
- } catch (error) {
132
- throw new Error(`Audit execution failed: ${error}`);
133
- }
134
- }
135
- async function getTier() {
136
- const wasm = await loadWasm();
137
- return wasm.get_tier();
138
- }
139
- async function getChecksLimit() {
140
- const wasm = await loadWasm();
141
- return wasm.get_checks_limit();
142
- }
143
- async function validateLicense(licenseKey) {
144
- const wasm = await loadWasm();
145
- return wasm.validate_license(licenseKey);
146
- }
147
- async function getVersion() {
148
- const wasm = await loadWasm();
149
- return wasm.version();
150
- }
151
- async function isReady() {
152
- try {
153
- const wasm = await loadWasm();
154
- return wasm.is_initialized();
155
- } catch {
156
- return false;
157
- }
158
- }
2
+ getPremiumChecks,
3
+ getVersion,
4
+ initEngine,
5
+ isReady
6
+ } from "./chunk-NF755NKY.mjs";
159
7
  export {
160
- getChecksLimit,
161
- getTier,
8
+ getPremiumChecks,
162
9
  getVersion,
163
10
  initEngine,
164
- isReady,
165
- runAudit,
166
- validateLicense
11
+ isReady
167
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankcli/cli",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "RankCLI - Ship code, get ranked. SEO meets CI/CD.",
5
5
  "homepage": "https://rankcli.dev",
6
6
  "main": "dist/index.js",
@@ -36,6 +36,7 @@
36
36
  "build:wasm:dev": "cd native && wasm-pack build --target nodejs --dev && mkdir -p ../wasm && cp -r pkg/* ../wasm/",
37
37
  "prebuild:wasm": "mkdir -p wasm",
38
38
  "inject-config": "node scripts/inject-config.js",
39
+ "build:sea": "bash scripts/build-sea.sh",
39
40
  "prepublishOnly": "npm run build && npm run inject-config",
40
41
  "start": "node dist/index.js",
41
42
  "lint": "eslint src/",
@@ -44,7 +45,7 @@
44
45
  "update-version": "node scripts/update-version.js"
45
46
  },
46
47
  "dependencies": {
47
- "@rankcli/agent-runtime": "^0.0.15",
48
+ "@rankcli/agent-runtime": "^0.0.17",
48
49
  "@supabase/supabase-js": "^2.45.0",
49
50
  "chalk": "^5.3.0",
50
51
  "commander": "^12.0.0",
@@ -59,6 +60,8 @@
59
60
  "devDependencies": {
60
61
  "@types/inquirer": "^9.0.0",
61
62
  "@types/node": "^20.0.0",
63
+ "esbuild": "^0.28.2",
64
+ "postject": "^1.0.0-alpha.6",
62
65
  "tsup": "^8.0.0",
63
66
  "tsx": "^4.0.0",
64
67
  "typescript": "^5.3.0",
@@ -1,10 +0,0 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
- export {
9
- __require
10
- };