recker 1.0.79 → 1.0.80-next.70ea84d
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/dist/browser/browser/mini.d.ts +2 -2
- package/dist/browser/browser/recker-mini.d.ts +8 -8
- package/dist/browser/browser/recker.d.ts +11 -8
- package/dist/browser/browser/recker.js +54 -6
- package/dist/browser/core/client.d.ts +15 -10
- package/dist/browser/core/client.js +54 -38
- package/dist/browser/index.iife.min.js +129 -130
- package/dist/browser/index.min.js +129 -130
- package/dist/browser/index.mini.iife.js +5697 -636
- package/dist/browser/index.mini.iife.min.js +47 -48
- package/dist/browser/index.mini.min.js +47 -48
- package/dist/browser/index.mini.umd.js +5697 -636
- package/dist/browser/index.mini.umd.min.js +47 -48
- package/dist/browser/index.umd.min.js +129 -130
- package/dist/browser/mini.d.ts +2 -2
- package/dist/browser/plugins/proxy-rotator.d.ts +2 -2
- package/dist/browser/plugins/proxy-rotator.js +6 -28
- package/dist/browser/recker-mini.d.ts +8 -8
- package/dist/browser/recker.d.ts +11 -8
- package/dist/browser/recker.js +54 -6
- package/dist/browser/scrape/document.js +2 -2
- package/dist/browser/scrape/element.js +7 -1
- package/dist/browser/scrape/parser/nodes/html.js +1 -1
- package/dist/browser/scrape/spider.d.ts +52 -0
- package/dist/browser/scrape/spider.js +620 -38
- package/dist/browser/scrape/types.d.ts +2 -0
- package/dist/browser/search/google.d.ts +26 -1
- package/dist/browser/search/google.js +427 -45
- package/dist/browser/seo/analyzer.d.ts +1 -0
- package/dist/browser/seo/analyzer.js +144 -1
- package/dist/browser/seo/index.d.ts +1 -1
- package/dist/browser/seo/keyword-campaign-analyzer.d.ts +2 -0
- package/dist/browser/seo/keyword-campaign-analyzer.js +538 -0
- package/dist/browser/seo/keyword-campaign-seed-advanced.d.ts +17 -0
- package/dist/browser/seo/keyword-campaign-seed-advanced.js +269 -0
- package/dist/browser/seo/keyword-campaign-seed-core.d.ts +29 -0
- package/dist/browser/seo/keyword-campaign-seed-core.js +525 -0
- package/dist/browser/seo/keyword-campaign-shared.d.ts +165 -0
- package/dist/browser/seo/keyword-campaign-shared.js +59 -0
- package/dist/browser/seo/keyword-campaign.d.ts +4 -107
- package/dist/browser/seo/keyword-campaign.js +2 -380
- package/dist/browser/seo/keywords.js +5 -22
- package/dist/browser/seo/types.d.ts +19 -0
- package/dist/browser/transport/curl.d.ts +5 -1
- package/dist/browser/transport/curl.js +207 -50
- package/dist/browser/transport/undici.d.ts +4 -3
- package/dist/browser/transport/undici.js +123 -49
- package/dist/browser/types/index.d.ts +9 -3
- package/dist/browser/utils/binary-manager.js +26 -3
- package/dist/browser/utils/block-detector.d.ts +8 -0
- package/dist/browser/utils/block-detector.js +542 -7
- package/dist/cli/commands/hls-runner.js +5 -4
- package/dist/cli/commands/live-runner.js +5 -4
- package/dist/cli/commands/loadtest-runner.js +3 -2
- package/dist/cli/commands/search.d.ts +2 -0
- package/dist/cli/commands/search.js +105 -0
- package/dist/cli/commands/seo-runner.js +9 -7
- package/dist/cli/commands/seo.js +140 -1
- package/dist/cli/commands/serve.js +75 -131
- package/dist/cli/commands/server-runner.js +59 -82
- package/dist/cli/commands/spider-runner.d.ts +37 -1
- package/dist/cli/commands/spider-runner.js +134 -10
- package/dist/cli/commands/spider.d.ts +18 -1
- package/dist/cli/commands/spider.js +457 -27
- package/dist/cli/events/handlers/cli.js +30 -1
- package/dist/cli/events/handlers/tui.js +26 -0
- package/dist/cli/events/types.d.ts +27 -0
- package/dist/cli/handler.d.ts +2 -12
- package/dist/cli/handler.js +20 -15
- package/dist/cli/handlers/protocols.js +39 -12
- package/dist/cli/handlers/search.d.ts +2 -0
- package/dist/cli/handlers/search.js +171 -0
- package/dist/cli/handlers/seo-analyze.d.ts +1 -0
- package/dist/cli/handlers/seo-analyze.js +666 -0
- package/dist/cli/handlers/seo-robots.d.ts +1 -0
- package/dist/cli/handlers/seo-robots.js +76 -0
- package/dist/cli/handlers/seo-serp.d.ts +54 -0
- package/dist/cli/handlers/seo-serp.js +243 -0
- package/dist/cli/handlers/seo-sitemap.d.ts +1 -0
- package/dist/cli/handlers/seo-sitemap.js +55 -0
- package/dist/cli/handlers/seo-spider.d.ts +1 -0
- package/dist/cli/handlers/seo-spider.js +334 -0
- package/dist/cli/handlers/seo.d.ts +8 -4
- package/dist/cli/handlers/seo.js +294 -442
- package/dist/cli/handlers/spider.js +94 -17
- package/dist/cli/handlers/streaming.js +5 -1
- package/dist/cli/index.js +11 -2
- package/dist/cli/presets.d.ts +1 -1
- package/dist/cli/presets.js +15 -4
- package/dist/cli/tui/app.js +6 -1
- package/dist/cli/tui/components/rich-response.d.ts +72 -0
- package/dist/cli/tui/components/rich-response.js +117 -0
- package/dist/cli/tui/executor-commands/background.js +30 -24
- package/dist/cli/tui/executor-commands/testing.js +3 -2
- package/dist/cli/tui/hooks/useDomains.d.ts +17 -0
- package/dist/cli/tui/hooks/useHelp.js +15 -2
- package/dist/cli/tui/job-manager.d.ts +4 -4
- package/dist/cli/tui/job-manager.js +5 -1
- package/dist/cli/tui/spider-tui.d.ts +63 -0
- package/dist/cli/tui/spider-tui.js +120 -2
- package/dist/cli/types.d.ts +12 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/utils/option-helpers.d.ts +10 -0
- package/dist/cli/utils/option-helpers.js +63 -0
- package/dist/cli/utils/score-color.d.ts +11 -0
- package/dist/cli/utils/score-color.js +11 -0
- package/dist/cli/utils/serp-campaign.d.ts +53 -0
- package/dist/cli/utils/serp-campaign.js +53 -0
- package/dist/cli/utils/serp-config.d.ts +26 -0
- package/dist/cli/utils/serp-config.js +125 -0
- package/dist/core/client.d.ts +15 -10
- package/dist/core/client.js +54 -38
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/cli.js +35 -34
- package/dist/mcp/client.d.ts +2 -2
- package/dist/mcp/client.js +20 -2
- package/dist/mcp/contract.d.ts +1 -1
- package/dist/mcp/profiles.js +5 -1
- package/dist/mcp/prompts/index.js +8 -4
- package/dist/mcp/resources/index.js +46 -23
- package/dist/mcp/server.js +9 -6
- package/dist/mcp/tools/protocols.js +9 -2
- package/dist/mcp/tools/registry.js +13 -3
- package/dist/mcp/tools/seo.js +427 -2
- package/dist/mcp/types.d.ts +5 -1
- package/dist/plugins/proxy-rotator.d.ts +2 -2
- package/dist/plugins/proxy-rotator.js +6 -28
- package/dist/raffel/client.d.ts +38 -0
- package/dist/raffel/client.js +282 -0
- package/dist/raffel/index.d.ts +2 -0
- package/dist/raffel/index.js +2 -0
- package/dist/raffel/types.d.ts +40 -0
- package/dist/raffel/types.js +14 -0
- package/dist/recker.d.ts +13 -7
- package/dist/recker.js +58 -6
- package/dist/scrape/document.js +2 -2
- package/dist/scrape/element.js +7 -1
- package/dist/scrape/parser/nodes/html.js +1 -1
- package/dist/scrape/spider.d.ts +52 -0
- package/dist/scrape/spider.js +620 -38
- package/dist/scrape/types.d.ts +2 -0
- package/dist/search/google.d.ts +26 -1
- package/dist/search/google.js +427 -45
- package/dist/search/index.d.ts +1 -1
- package/dist/seo/analyzer.d.ts +1 -0
- package/dist/seo/analyzer.js +144 -1
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/keyword-campaign-analyzer.d.ts +2 -0
- package/dist/seo/keyword-campaign-analyzer.js +538 -0
- package/dist/seo/keyword-campaign-seed-advanced.d.ts +17 -0
- package/dist/seo/keyword-campaign-seed-advanced.js +269 -0
- package/dist/seo/keyword-campaign-seed-core.d.ts +29 -0
- package/dist/seo/keyword-campaign-seed-core.js +525 -0
- package/dist/seo/keyword-campaign-shared.d.ts +165 -0
- package/dist/seo/keyword-campaign-shared.js +59 -0
- package/dist/seo/keyword-campaign.d.ts +4 -107
- package/dist/seo/keyword-campaign.js +2 -380
- package/dist/seo/keywords.js +5 -22
- package/dist/seo/types.d.ts +19 -0
- package/dist/template/index.d.ts +1 -1
- package/dist/template/types.d.ts +0 -2
- package/dist/testing/index.d.ts +1 -22
- package/dist/testing/index.js +1 -11
- package/dist/transport/curl.d.ts +5 -1
- package/dist/transport/curl.js +207 -50
- package/dist/transport/undici.d.ts +4 -3
- package/dist/transport/undici.js +123 -49
- package/dist/types/index.d.ts +9 -3
- package/dist/utils/binary-manager.js +26 -3
- package/dist/utils/block-detector.d.ts +8 -0
- package/dist/utils/block-detector.js +542 -7
- package/dist/version.js +1 -1
- package/package.json +12 -1
- package/dist/testing/mock-dns-server.d.ts +0 -69
- package/dist/testing/mock-dns-server.js +0 -269
- package/dist/testing/mock-ftp-server.d.ts +0 -89
- package/dist/testing/mock-ftp-server.js +0 -562
- package/dist/testing/mock-hls-server.d.ts +0 -80
- package/dist/testing/mock-hls-server.js +0 -381
- package/dist/testing/mock-http-server.d.ts +0 -124
- package/dist/testing/mock-http-server.js +0 -343
- package/dist/testing/mock-proxy-server.d.ts +0 -108
- package/dist/testing/mock-proxy-server.js +0 -615
- package/dist/testing/mock-sse-server.d.ts +0 -76
- package/dist/testing/mock-sse-server.js +0 -291
- package/dist/testing/mock-telnet-server.d.ts +0 -59
- package/dist/testing/mock-telnet-server.js +0 -274
- package/dist/testing/mock-udp-server.d.ts +0 -43
- package/dist/testing/mock-udp-server.js +0 -188
- package/dist/testing/mock-websocket-server.d.ts +0 -76
- package/dist/testing/mock-websocket-server.js +0 -334
- package/dist/testing/mock-whois-server.d.ts +0 -56
- package/dist/testing/mock-whois-server.js +0 -234
- package/dist/testing/proxy-certs.d.ts +0 -19
- package/dist/testing/proxy-certs.js +0 -208
|
@@ -1,25 +1,43 @@
|
|
|
1
1
|
import { createOutput } from '../handler-types.js';
|
|
2
2
|
import colors from '../../utils/colors.js';
|
|
3
|
+
import { parseSpiderSerpConfig } from '../utils/serp-config.js';
|
|
4
|
+
import { runSpiderKeywordCampaign, toSerpPayload } from '../utils/serp-campaign.js';
|
|
5
|
+
import { getOptionValue, toBoolean, toNonNegativeInt, toOptionString, toSpiderTransport, toSpiderFocusMode, } from '../utils/option-helpers.js';
|
|
3
6
|
function isExtendedContext(tui) {
|
|
4
|
-
return tui && typeof tui.setLoading === 'function';
|
|
7
|
+
return !!tui && typeof tui.setLoading === 'function';
|
|
5
8
|
}
|
|
6
9
|
export const spiderHandler = async (ctx) => {
|
|
7
10
|
const out = createOutput(ctx);
|
|
8
11
|
const extCtx = isExtendedContext(ctx.tui) ? ctx.tui : null;
|
|
12
|
+
const options = ctx.result.options;
|
|
13
|
+
const serpConfig = parseSpiderSerpConfig(options);
|
|
14
|
+
const shouldRunSerp = !!ctx.result.options.seo && serpConfig.enabled;
|
|
9
15
|
let url = ctx.result.positional.url;
|
|
10
|
-
const depth = ctx.result.options.depth
|
|
11
|
-
const limit = ctx.result.options.limit
|
|
12
|
-
const concurrency = ctx.result.options.concurrency
|
|
13
|
-
const output = ctx.result.options.output;
|
|
14
|
-
const focus = ctx.result.options.focus
|
|
15
|
-
const seo = ctx.result.options.seo;
|
|
16
|
-
const robots = ctx.result.options.robots;
|
|
17
|
-
const jsonOutput = ctx.result.options.json;
|
|
18
|
-
const jsonl = ctx.result.options.jsonl;
|
|
19
|
-
const extract = ctx.result.options.extract;
|
|
20
|
-
const include = ctx.result.options.include;
|
|
21
|
-
const exclude = ctx.result.options.exclude;
|
|
22
|
-
const noSitemap = ctx.result.options.noSitemap;
|
|
16
|
+
const depth = toNonNegativeInt(ctx.result.options.depth, 10);
|
|
17
|
+
const limit = toNonNegativeInt(ctx.result.options.limit, 1000);
|
|
18
|
+
const concurrency = toNonNegativeInt(ctx.result.options.concurrency, 5);
|
|
19
|
+
const output = toOptionString(ctx.result.options.output);
|
|
20
|
+
const focus = toSpiderFocusMode(ctx.result.options.focus);
|
|
21
|
+
const seo = toBoolean(ctx.result.options.seo, false);
|
|
22
|
+
const robots = toBoolean(ctx.result.options.robots, false);
|
|
23
|
+
const jsonOutput = toBoolean(ctx.result.options.json, false);
|
|
24
|
+
const jsonl = toBoolean(ctx.result.options.jsonl, false);
|
|
25
|
+
const extract = toOptionString(ctx.result.options.extract);
|
|
26
|
+
const include = toOptionString(ctx.result.options.include);
|
|
27
|
+
const exclude = toOptionString(ctx.result.options.exclude);
|
|
28
|
+
const noSitemap = toBoolean(ctx.result.options.noSitemap, false);
|
|
29
|
+
const transport = toSpiderTransport(getOptionValue(options, 'transport'));
|
|
30
|
+
const preferCurlFirst = toBoolean(getOptionValue(options, 'preferCurlFirst', 'prefer-curl-first'), true);
|
|
31
|
+
const timeout = toNonNegativeInt(getOptionValue(options, 'timeout'), 10000);
|
|
32
|
+
const delay = toNonNegativeInt(getOptionValue(options, 'delay'), 100);
|
|
33
|
+
const maxRetryAttempts = toNonNegativeInt(getOptionValue(options, 'maxRetryAttempts', 'max-retry-attempts'), 3);
|
|
34
|
+
const baseRetryDelayMs = toNonNegativeInt(getOptionValue(options, 'baseRetryDelayMs', 'base-retry-delay-ms'), 1000);
|
|
35
|
+
const maxRetryDelayMs = toNonNegativeInt(getOptionValue(options, 'maxRetryDelayMs', 'max-retry-delay-ms'), 12000);
|
|
36
|
+
const retryBackoffMultiplier = toNonNegativeInt(getOptionValue(options, 'retryBackoffMultiplier', 'retry-backoff-multiplier'), 2);
|
|
37
|
+
const retryJitterMs = toNonNegativeInt(getOptionValue(options, 'retryJitterMs', 'retry-jitter-ms'), 250);
|
|
38
|
+
const maxDomainBlockStrikes = toNonNegativeInt(getOptionValue(options, 'maxDomainBlockStrikes', 'max-domain-block-strikes'), 2);
|
|
39
|
+
const rotateUserAgent = toBoolean(getOptionValue(options, 'rotateUserAgent', 'rotate-user-agent'), true);
|
|
40
|
+
const randomizeHeaders = toBoolean(getOptionValue(options, 'randomizeHeaders', 'randomize-headers'), true);
|
|
23
41
|
if (!url) {
|
|
24
42
|
out.error(colors.red('URL is required'));
|
|
25
43
|
if (!ctx.isTui)
|
|
@@ -39,12 +57,26 @@ export const spiderHandler = async (ctx) => {
|
|
|
39
57
|
robots,
|
|
40
58
|
seo,
|
|
41
59
|
useSitemap: seo && !noSitemap,
|
|
42
|
-
focus
|
|
60
|
+
focus,
|
|
43
61
|
extract: extract ? extract.split(',').map(s => s.trim()) : undefined,
|
|
44
62
|
include: include ? include.split(',').map(s => s.trim()) : undefined,
|
|
45
63
|
exclude: exclude ? exclude.split(',').map(s => s.trim()) : undefined,
|
|
64
|
+
transport,
|
|
65
|
+
preferCurlFirst,
|
|
66
|
+
timeout,
|
|
67
|
+
delay,
|
|
68
|
+
maxRetryAttempts,
|
|
69
|
+
baseRetryDelayMs,
|
|
70
|
+
maxRetryDelayMs,
|
|
71
|
+
retryBackoffMultiplier,
|
|
72
|
+
retryJitterMs,
|
|
73
|
+
maxDomainBlockStrikes,
|
|
74
|
+
rotateUserAgent,
|
|
75
|
+
randomizeHeaders,
|
|
46
76
|
json: true,
|
|
47
77
|
});
|
|
78
|
+
const serpCampaign = shouldRunSerp ? await runSpiderKeywordCampaign(url, result.pages, serpConfig) : undefined;
|
|
79
|
+
const serpPayload = serpCampaign ? toSerpPayload(serpCampaign) : undefined;
|
|
48
80
|
extCtx.addResponse({
|
|
49
81
|
url: result.url,
|
|
50
82
|
pagesVisited: result.pagesVisited,
|
|
@@ -52,6 +84,8 @@ export const spiderHandler = async (ctx) => {
|
|
|
52
84
|
errors: result.errors,
|
|
53
85
|
internalLinks: result.internalLinks,
|
|
54
86
|
externalLinks: result.externalLinks,
|
|
87
|
+
security: result.security,
|
|
88
|
+
serp: serpPayload,
|
|
55
89
|
seo: result.seo ? {
|
|
56
90
|
avgScore: result.seo.avgScore,
|
|
57
91
|
pagesWithErrors: result.seo.pagesWithErrors,
|
|
@@ -68,7 +102,7 @@ export const spiderHandler = async (ctx) => {
|
|
|
68
102
|
}
|
|
69
103
|
catch (err) {
|
|
70
104
|
extCtx.addResponse({
|
|
71
|
-
error: err.message,
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
72
106
|
}, { responseType: 'spider-error' });
|
|
73
107
|
}
|
|
74
108
|
finally {
|
|
@@ -92,12 +126,25 @@ export const spiderHandler = async (ctx) => {
|
|
|
92
126
|
extract,
|
|
93
127
|
include,
|
|
94
128
|
exclude,
|
|
129
|
+
transport,
|
|
130
|
+
preferCurlFirst,
|
|
131
|
+
timeout,
|
|
132
|
+
delay,
|
|
133
|
+
maxRetryAttempts,
|
|
134
|
+
baseRetryDelayMs,
|
|
135
|
+
maxRetryDelayMs,
|
|
136
|
+
retryBackoffMultiplier,
|
|
137
|
+
retryJitterMs,
|
|
138
|
+
maxDomainBlockStrikes,
|
|
139
|
+
rotateUserAgent,
|
|
140
|
+
randomizeHeaders,
|
|
95
141
|
noSitemap,
|
|
142
|
+
serpConfig,
|
|
96
143
|
disableTui: false,
|
|
97
144
|
});
|
|
98
145
|
}
|
|
99
146
|
catch (err) {
|
|
100
|
-
out.error(colors.red(`Spider failed: ${err.message}`));
|
|
147
|
+
out.error(colors.red(`Spider failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
101
148
|
if (!ctx.isTui)
|
|
102
149
|
process.exit(1);
|
|
103
150
|
}
|
|
@@ -126,11 +173,41 @@ export const spiderCommands = {
|
|
|
126
173
|
include: { short: 'i', type: 'string', description: 'URL patterns to include (comma-separated regex)' },
|
|
127
174
|
exclude: { short: 'x', type: 'string', description: 'URL patterns to exclude (comma-separated regex)' },
|
|
128
175
|
noSitemap: { short: 'N', type: 'boolean', description: 'Disable sitemap.xml crawling' },
|
|
176
|
+
transport: { type: 'string', default: 'auto', description: 'HTTP transport (auto | undici | curl)' },
|
|
177
|
+
'prefer-curl-first': { type: 'boolean', default: true, description: 'Prefer curl-impersonate first in auto mode' },
|
|
178
|
+
timeout: { type: 'number', default: 10000, description: 'Request timeout in ms' },
|
|
179
|
+
delay: { type: 'number', default: 100, description: 'Delay between requests in ms' },
|
|
180
|
+
'max-retry-attempts': { type: 'number', default: 3, description: 'Max retry attempts per request' },
|
|
181
|
+
'base-retry-delay-ms': { type: 'number', default: 1000, description: 'Base retry delay in ms' },
|
|
182
|
+
'max-retry-delay-ms': { type: 'number', default: 12000, description: 'Maximum retry delay in ms' },
|
|
183
|
+
'retry-backoff-multiplier': { type: 'number', default: 2, description: 'Retry backoff multiplier' },
|
|
184
|
+
'retry-jitter-ms': { type: 'number', default: 250, description: 'Retry jitter in ms' },
|
|
185
|
+
'max-domain-block-strikes': { type: 'number', default: 2, description: 'Block strikes before forcing curl in auto mode' },
|
|
186
|
+
'rotate-user-agent': { type: 'boolean', default: true, description: 'Rotate user-agent per request' },
|
|
187
|
+
'randomize-headers': { type: 'boolean', default: true, description: 'Randomize request headers' },
|
|
188
|
+
serp: { type: 'boolean', description: 'Enable SERP campaign for top extracted keywords' },
|
|
189
|
+
'serp-top-keywords': { type: 'number', default: 5, description: 'Top keywords per page used as seeds' },
|
|
190
|
+
'serp-query-limit': { type: 'number', default: 10, description: 'Number of SERP queries to execute' },
|
|
191
|
+
'serp-results-per-query': { type: 'number', default: 10, description: 'Results fetched per SERP query' },
|
|
192
|
+
'serp-concurrency': { type: 'number', default: 1, description: 'Number of SERP queries to execute in parallel' },
|
|
193
|
+
'serp-delay-ms': { type: 'number', default: 1200, description: 'Delay in ms between SERP queries' },
|
|
194
|
+
'serp-delay-jitter-ms': { type: 'number', default: 450, description: 'Random delay jitter in ms between SERP queries' },
|
|
195
|
+
'serp-max-consecutive-blocks': { type: 'number', default: 3, description: 'Stop campaign after this many blocked/captcha responses in a row' },
|
|
196
|
+
'serp-captcha-cooldown-ms': { type: 'number', default: 2400, description: 'Cooldown in ms after captcha before continuing SERP' },
|
|
197
|
+
'serp-retry-count': { type: 'number', default: 1, description: 'Retry count per SERP query' },
|
|
198
|
+
'serp-retry-delay-ms': { type: 'number', default: 1200, description: 'Base retry delay in ms for SERP queries' },
|
|
199
|
+
'serp-transport': { type: 'string', description: 'Search transport (auto | undici | curl)' },
|
|
200
|
+
'serp-timeout': { type: 'number', description: 'Search timeout in ms' },
|
|
201
|
+
'serp-country': { type: 'string', description: 'Search country/region' },
|
|
202
|
+
'serp-gl': { type: 'string', description: 'Google GL parameter' },
|
|
203
|
+
'serp-hl': { type: 'string', description: 'Google HL parameter' },
|
|
129
204
|
},
|
|
130
205
|
examples: [
|
|
131
206
|
{ cmd: 'rek spider example.com', desc: 'Basic crawl' },
|
|
132
207
|
{ cmd: 'rek spider example.com --seo', desc: 'Enable SEO analysis' },
|
|
133
208
|
{ cmd: 'rek spider example.com -d 3 -l 50', desc: 'Depth 3, max 50 pages' },
|
|
209
|
+
{ cmd: 'rek spider example.com --seo --serp', desc: 'Enable SEO + SERP campaign' },
|
|
210
|
+
{ cmd: 'rek spider example.com --seo --serp --serp-top-keywords 12 --serp-query-limit 10 --serp-results-per-query 10', desc: 'Tuned SERP campaign' },
|
|
134
211
|
],
|
|
135
212
|
handler: spiderHandler
|
|
136
213
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withHandler, getString, getNumber, getBoolean, colors, } from '../output.js';
|
|
2
|
+
import { ValidationError } from '../../core/errors.js';
|
|
2
3
|
export const hlsInfoHandler = withHandler({ loading: true }, async (ctx, out, extCtx) => {
|
|
3
4
|
let url = getString(ctx.result.positional.url);
|
|
4
5
|
if (!url) {
|
|
@@ -83,7 +84,10 @@ export const sseHandler = withHandler({ loading: true }, async (ctx, out, extCtx
|
|
|
83
84
|
});
|
|
84
85
|
const reader = response.body?.getReader();
|
|
85
86
|
if (!reader) {
|
|
86
|
-
throw new
|
|
87
|
+
throw new ValidationError('SSE response must include a readable body', {
|
|
88
|
+
field: 'response.body',
|
|
89
|
+
value: url,
|
|
90
|
+
});
|
|
87
91
|
}
|
|
88
92
|
const decoder = new TextDecoder();
|
|
89
93
|
const events = [];
|
package/dist/cli/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerDnsCommands } from './commands/dns.js';
|
|
|
8
8
|
import { registerAiCommand } from './commands/ai.js';
|
|
9
9
|
import { registerSpiderCommand } from './commands/spider.js';
|
|
10
10
|
import { registerScrapeCommand } from './commands/scrape.js';
|
|
11
|
+
import { registerSearchCommand } from './commands/search.js';
|
|
11
12
|
import { registerBenchCommand, registerLoadCommand } from './commands/bench.js';
|
|
12
13
|
import { registerSecurityCommand } from './commands/security.js';
|
|
13
14
|
import { registerServeCommand } from './commands/serve.js';
|
|
@@ -123,6 +124,7 @@ async function main() {
|
|
|
123
124
|
.option('-o, --output <file>', 'Write response body to file')
|
|
124
125
|
.option('-j, --json', 'Force JSON content-type')
|
|
125
126
|
.option('-e, --env [path]', 'Load .env file')
|
|
127
|
+
.option('-x, --proxy <url>', 'Proxy URL (http://, https://, socks5://, socks5h://). Repeat for rotation.')
|
|
126
128
|
.addHelpText('after', () => `
|
|
127
129
|
${theme.header('Examples:')}
|
|
128
130
|
${theme.example('$ rek httpbin.org/json')}
|
|
@@ -144,6 +146,12 @@ ${formatColumns(PRESET_NAMES, { prefix: '@', indent: 2, minWidth: 16, transform:
|
|
|
144
146
|
const { clientOptions: enhancerOptions, remainingArgs } = await parseEnhancerPresets(args);
|
|
145
147
|
let argsToParse = remainingArgs;
|
|
146
148
|
let finalClientOptions = enhancerOptions;
|
|
149
|
+
if (options.proxy) {
|
|
150
|
+
const proxyArg = options.proxy;
|
|
151
|
+
finalClientOptions.proxy = Array.isArray(proxyArg)
|
|
152
|
+
? proxyArg
|
|
153
|
+
: proxyArg;
|
|
154
|
+
}
|
|
147
155
|
if (argsToParse.length > 0 && argsToParse[0].startsWith('@')) {
|
|
148
156
|
let presetArg = argsToParse[0];
|
|
149
157
|
let presetName = presetArg.slice(1);
|
|
@@ -155,7 +163,7 @@ ${formatColumns(PRESET_NAMES, { prefix: '@', indent: 2, minWidth: 16, transform:
|
|
|
155
163
|
}
|
|
156
164
|
const mainPresetOptions = await resolvePreset(presetName);
|
|
157
165
|
if (mainPresetOptions) {
|
|
158
|
-
finalClientOptions = { ...finalClientOptions, ...mainPresetOptions, headers: { ...finalClientOptions.headers, ...mainPresetOptions.headers } };
|
|
166
|
+
finalClientOptions = { ...finalClientOptions, ...mainPresetOptions, headers: { ...finalClientOptions.headers, ...(mainPresetOptions.headers || {}) } };
|
|
159
167
|
if (pathFromPreset) {
|
|
160
168
|
if (finalClientOptions.baseUrl) {
|
|
161
169
|
finalClientOptions.baseUrl = (finalClientOptions.baseUrl || '').replace(/\/$/, '') + pathFromPreset;
|
|
@@ -259,7 +267,7 @@ ${formatColumns(PRESET_NAMES, { prefix: '@', indent: 2, minWidth: 16, transform:
|
|
|
259
267
|
body = data;
|
|
260
268
|
}
|
|
261
269
|
await handleRequest({
|
|
262
|
-
method,
|
|
270
|
+
method: method,
|
|
263
271
|
url,
|
|
264
272
|
body,
|
|
265
273
|
verbose: options.verbose,
|
|
@@ -314,6 +322,7 @@ ${formatColumns(PRESET_NAMES, { prefix: '@', indent: 2, minWidth: 16, transform:
|
|
|
314
322
|
registerSecurityCommand(program);
|
|
315
323
|
registerSpiderCommand(program);
|
|
316
324
|
registerScrapeCommand(program);
|
|
325
|
+
registerSearchCommand(program);
|
|
317
326
|
registerSeoCommand(program);
|
|
318
327
|
registerNetworkCommands(program);
|
|
319
328
|
registerDnsCommands(program);
|
package/dist/cli/presets.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export interface ResolvePresetOptions {
|
|
|
2
2
|
throwOnError?: boolean;
|
|
3
3
|
silent?: boolean;
|
|
4
4
|
}
|
|
5
|
-
export declare function resolvePreset(name: string, options?: ResolvePresetOptions): Promise<
|
|
5
|
+
export declare function resolvePreset(name: string, options?: ResolvePresetOptions): Promise<unknown>;
|
package/dist/cli/presets.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as presets from '../presets/index.js';
|
|
2
2
|
import colors from '../utils/colors.js';
|
|
3
|
+
import { ConfigurationError, ValidationError } from '../core/errors.js';
|
|
3
4
|
const ENV_MAPPING = {
|
|
4
5
|
openai: ['OPENAI_API_KEY'],
|
|
5
6
|
anthropic: ['ANTHROPIC_API_KEY'],
|
|
@@ -20,13 +21,14 @@ const ENV_MAPPING = {
|
|
|
20
21
|
together: ['TOGETHER_API_KEY'],
|
|
21
22
|
perplexity: ['PERPLEXITY_API_KEY'],
|
|
22
23
|
};
|
|
24
|
+
const presetFactories = presets;
|
|
23
25
|
export async function resolvePreset(name, options = {}) {
|
|
24
26
|
const { throwOnError = false, silent = false } = options;
|
|
25
|
-
const presetFn =
|
|
27
|
+
const presetFn = presetFactories[name];
|
|
26
28
|
if (!presetFn) {
|
|
27
29
|
const msg = `Unknown preset '@${name}'`;
|
|
28
30
|
if (throwOnError) {
|
|
29
|
-
throw new
|
|
31
|
+
throw new ValidationError(msg, { field: 'name', value: name });
|
|
30
32
|
}
|
|
31
33
|
if (!silent) {
|
|
32
34
|
console.error(colors.red(`Error: ${msg}`));
|
|
@@ -58,9 +60,9 @@ export async function resolvePreset(name, options = {}) {
|
|
|
58
60
|
return presetFn(presetOptions);
|
|
59
61
|
}
|
|
60
62
|
catch (error) {
|
|
61
|
-
const msg = `Error initializing preset @${name}: ${error
|
|
63
|
+
const msg = `Error initializing preset @${name}: ${getErrorMessage(error)}`;
|
|
62
64
|
if (throwOnError) {
|
|
63
|
-
throw new
|
|
65
|
+
throw new ConfigurationError(msg, { configKey: `preset.${name}` });
|
|
64
66
|
}
|
|
65
67
|
if (!silent) {
|
|
66
68
|
console.error(colors.red(msg));
|
|
@@ -68,6 +70,15 @@ export async function resolvePreset(name, options = {}) {
|
|
|
68
70
|
return null;
|
|
69
71
|
}
|
|
70
72
|
}
|
|
73
|
+
function getErrorMessage(error) {
|
|
74
|
+
if (error instanceof Error) {
|
|
75
|
+
return error.message;
|
|
76
|
+
}
|
|
77
|
+
if (typeof error === 'string') {
|
|
78
|
+
return error;
|
|
79
|
+
}
|
|
80
|
+
return String(error);
|
|
81
|
+
}
|
|
71
82
|
function mapEnvToOption(preset, env) {
|
|
72
83
|
if (env.endsWith('_KEY') || env.endsWith('_TOKEN'))
|
|
73
84
|
return 'apiKey';
|
package/dist/cli/tui/app.js
CHANGED
|
@@ -385,7 +385,12 @@ function DomainsTabContent({ width, height }) {
|
|
|
385
385
|
}
|
|
386
386
|
return Box({ flexDirection: 'column', paddingX: 1 }, Text({ color: primaryColor, bold: true }, `🌐 ${intel.domain}`), Text({ color: mutedFg }, '─'.repeat(Math.min(60, width - 4))), Text({ color: mutedFg, dim: true }, `First seen: ${intel.firstSeen.toLocaleString()} │ Last: ${intel.lastAccessed.toLocaleString()}`), Box({ height: 1 }), intel.dns ? Box({ flexDirection: 'column' }, Text({ color: accentColor, bold: true }, '📡 DNS Records'), ...intel.dns.records.slice(0, 5).map(r => Text({ color: mutedFg }, ` ${r.type.padEnd(6)} ${r.value}`)), intel.dns.records.length > 5
|
|
387
387
|
? Text({ color: mutedFg, dim: true }, ` ... and ${intel.dns.records.length - 5} more`)
|
|
388
|
-
: null, Box({ height: 1 })) : null, intel.seo ? Box({ flexDirection: 'column' }, Text({ color: accentColor, bold: true }, `📊 SEO Score: ${intel.seo.score}/100`), Text({ color: mutedFg }, ` Issues: ${intel.seo.issues} │ Technical: ${intel.seo.categories.technical} │ Content: ${intel.seo.categories.content}`), Box({ height: 1 })) : null, intel.spider ? Box({ flexDirection: 'column' }, Text({ color: accentColor, bold: true }, `🕷 Spider Results`), Text({ color: mutedFg }, ` Pages: ${intel.spider.pagesFound} │ Links: ${intel.spider.internalLinks + intel.spider.externalLinks} │ Images: ${intel.spider.images}`),
|
|
388
|
+
: null, Box({ height: 1 })) : null, intel.seo ? Box({ flexDirection: 'column' }, Text({ color: accentColor, bold: true }, `📊 SEO Score: ${intel.seo.score}/100`), Text({ color: mutedFg }, ` Issues: ${intel.seo.issues} │ Technical: ${intel.seo.categories.technical} │ Content: ${intel.seo.categories.content}`), Box({ height: 1 })) : null, intel.spider ? Box({ flexDirection: 'column' }, Text({ color: accentColor, bold: true }, `🕷 Spider Results`), Text({ color: mutedFg }, ` Pages: ${intel.spider.pagesFound} │ Links: ${intel.spider.internalLinks + intel.spider.externalLinks} │ Images: ${intel.spider.images}`), intel.spider.security ? Box({ flexDirection: 'column' }, Text({ color: mutedFg }, ` Anti-bot: blocked ${intel.spider.security.blockedPages} · captcha ${intel.spider.security.captchaPages}`), Text({ color: mutedFg }, ` Attempts: ${intel.spider.security.attempts} │ retries: ${intel.spider.security.retries}`), Text({ color: mutedFg }, ` Avg attempts: ${intel.spider.security.avgAttempts !== undefined ? intel.spider.security.avgAttempts.toFixed(2) : 'n/a'}`), Text({ color: mutedFg }, ` Transport: curl ${intel.spider.security.transportUsage?.curl || 0} / undici ${intel.spider.security.transportUsage?.undici || 0}`), Text({ color: mutedFg }, ` Timing: TTFB ${intel.spider.security.avgTtfbMs ?? 'n/a'}ms | total ${intel.spider.security.avgTotalMs ?? 'n/a'}ms | download ${intel.spider.security.avgDownloadMs ?? 'n/a'}ms`), intel.spider.security.captchaProviders && Object.keys(intel.spider.security.captchaProviders).length > 0
|
|
389
|
+
? Text({ color: mutedFg }, ` Captcha providers: ${Object.entries(intel.spider.security.captchaProviders)
|
|
390
|
+
.sort((a, b) => b[1] - a[1])
|
|
391
|
+
.map(([provider, count]) => `${provider}: ${count}`)
|
|
392
|
+
.join(' | ')}`)
|
|
393
|
+
: null, Box({ height: 1 })) : null, Box({ height: 1 })) : null, intel.downloads.length > 0 ? Box({ flexDirection: 'column' }, Text({ color: accentColor, bold: true }, `📥 Downloads (${intel.downloads.length})`), ...intel.downloads.slice(-3).map(d => Text({ color: mutedFg }, ` ${d.type.padEnd(5)} ${d.filename.slice(0, 30)} (${(d.size / 1024 / 1024).toFixed(1)} MB)`)), Box({ height: 1 })) : null, Text({ color: mutedFg, dim: true }, 'Esc: Back to list │ Ctrl+D: DNS │ Ctrl+S: SEO │ Ctrl+R: Crawl'));
|
|
389
394
|
}
|
|
390
395
|
return Box({ flexDirection: 'column', paddingX: 1 }, Text({ color: primaryColor, bold: true }, `🌐 Domain Intelligence (${stats.total})`), Text({ color: mutedFg }, '─'.repeat(Math.min(60, width - 4))), Box({ height: 1 }), Box({ flexDirection: 'row', gap: 2 }, Text({ color: mutedFg, dim: true }, `DNS: ${stats.withDns}`), Text({ color: mutedFg, dim: true }, `SEO: ${stats.withSeo}`), Text({ color: mutedFg, dim: true }, `Spider: ${stats.withSpider}`), Text({ color: mutedFg, dim: true }, `Downloads: ${stats.totalDownloads}`)), Box({ height: 1 }), Box({ flexDirection: 'row', gap: 1 }, Text({ color: mutedFg, dim: true, bold: true }, ' Domain'.padEnd(25)), Text({ color: mutedFg, dim: true, bold: true }, 'DNS'), Text({ color: mutedFg, dim: true, bold: true }, ' SEO'), Text({ color: mutedFg, dim: true, bold: true }, ' Spider'), Text({ color: mutedFg, dim: true, bold: true }, ' DL')), ...allDomains.slice(0, height - 10).map((entry, idx) => {
|
|
391
396
|
const isSelected = entry.domain === selected;
|
|
@@ -206,6 +206,61 @@ interface SeoResponseData {
|
|
|
206
206
|
};
|
|
207
207
|
checks?: SeoCheck[];
|
|
208
208
|
topIssues?: string[];
|
|
209
|
+
serp?: {
|
|
210
|
+
summary?: {
|
|
211
|
+
queriesRequested?: number;
|
|
212
|
+
queriesExecuted?: number;
|
|
213
|
+
queriesFound?: number;
|
|
214
|
+
queriesBlocked?: number;
|
|
215
|
+
queriesCaptcha?: number;
|
|
216
|
+
avgTopPosition?: number | null;
|
|
217
|
+
top3Count?: number;
|
|
218
|
+
top10Count?: number;
|
|
219
|
+
topOrganicCompetitors?: Array<{
|
|
220
|
+
domain: string;
|
|
221
|
+
matchedKeywords: number;
|
|
222
|
+
totalOutperformedQueries?: number;
|
|
223
|
+
organicQueries?: number;
|
|
224
|
+
paidQueries?: number;
|
|
225
|
+
avgOutperformedGap?: number | null;
|
|
226
|
+
}>;
|
|
227
|
+
topPaidCompetitors?: Array<{
|
|
228
|
+
domain: string;
|
|
229
|
+
matchedKeywords: number;
|
|
230
|
+
totalOutperformedQueries?: number;
|
|
231
|
+
organicQueries?: number;
|
|
232
|
+
paidQueries?: number;
|
|
233
|
+
avgOutperformedGap?: number | null;
|
|
234
|
+
}>;
|
|
235
|
+
competitorCoverage?: {
|
|
236
|
+
organicUniqueDomains?: number;
|
|
237
|
+
paidUniqueDomains?: number;
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
campaign?: {
|
|
241
|
+
active?: boolean;
|
|
242
|
+
confidence?: 'high' | 'medium' | 'low';
|
|
243
|
+
evidence?: string[];
|
|
244
|
+
};
|
|
245
|
+
results?: Array<{
|
|
246
|
+
keyword: string;
|
|
247
|
+
found: boolean;
|
|
248
|
+
blocked?: boolean;
|
|
249
|
+
blockReason?: string;
|
|
250
|
+
position: number | null;
|
|
251
|
+
targetUrl?: string;
|
|
252
|
+
searchUrl: string;
|
|
253
|
+
}>;
|
|
254
|
+
pageComparison?: Array<{
|
|
255
|
+
pageUrl: string;
|
|
256
|
+
tracked: number;
|
|
257
|
+
found: number;
|
|
258
|
+
appearanceRate: string;
|
|
259
|
+
avgPosition: string | number | null;
|
|
260
|
+
top3: number;
|
|
261
|
+
top10: number;
|
|
262
|
+
}>;
|
|
263
|
+
};
|
|
209
264
|
}
|
|
210
265
|
declare function renderSeoResponse(data: SeoResponseData, width: number): VNode;
|
|
211
266
|
interface SpiderResponseData {
|
|
@@ -266,6 +321,23 @@ interface SpiderResponseData {
|
|
|
266
321
|
};
|
|
267
322
|
};
|
|
268
323
|
};
|
|
324
|
+
serp?: NonNullable<SeoResponseData['serp']>;
|
|
325
|
+
security?: {
|
|
326
|
+
pages?: number;
|
|
327
|
+
blockedPages?: number;
|
|
328
|
+
captchaPages?: number;
|
|
329
|
+
captchaProviders?: Record<string, number>;
|
|
330
|
+
attempts?: number;
|
|
331
|
+
retries?: number;
|
|
332
|
+
transportUsage?: {
|
|
333
|
+
undici?: number;
|
|
334
|
+
curl?: number;
|
|
335
|
+
};
|
|
336
|
+
avgAttempts?: number;
|
|
337
|
+
avgTtfbMs?: number;
|
|
338
|
+
avgTotalMs?: number;
|
|
339
|
+
avgDownloadMs?: number;
|
|
340
|
+
};
|
|
269
341
|
}
|
|
270
342
|
declare function renderSpiderResponse(data: SpiderResponseData, width: number): VNode;
|
|
271
343
|
declare function renderError(data: {
|
|
@@ -313,6 +313,90 @@ function renderTlsResponse(data, width) {
|
|
|
313
313
|
}
|
|
314
314
|
return Box({ flexDirection: 'column', width }, ...children);
|
|
315
315
|
}
|
|
316
|
+
function toPercent(value) {
|
|
317
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
318
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
319
|
+
const parsed = typeof value === 'string' ? parseFloat(value) : NaN;
|
|
320
|
+
if (Number.isFinite(parsed))
|
|
321
|
+
return Math.max(0, Math.min(100, Math.round(parsed)));
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
function renderSerpSection(payload, width, isSpider = false) {
|
|
325
|
+
const serp = payload.serp;
|
|
326
|
+
if (!serp)
|
|
327
|
+
return [];
|
|
328
|
+
const summary = serp.summary || {};
|
|
329
|
+
const queriesRequested = Math.max(0, summary.queriesRequested || summary.queriesExecuted || 0);
|
|
330
|
+
const queriesFound = Math.max(0, summary.queriesFound || 0);
|
|
331
|
+
const queriesBlocked = Math.max(0, summary.queriesBlocked || 0);
|
|
332
|
+
const queriesCaptcha = Math.max(0, summary.queriesCaptcha || 0);
|
|
333
|
+
const foundRate = queriesRequested > 0 ? Math.round((queriesFound / queriesRequested) * 100) : 0;
|
|
334
|
+
const avgTop = summary.avgTopPosition;
|
|
335
|
+
const top3 = summary.top3Count || 0;
|
|
336
|
+
const top10 = summary.top10Count || 0;
|
|
337
|
+
const children = [];
|
|
338
|
+
children.push(Box({ marginTop: 1 }, Text({ color: themeColor('foreground'), bold: true }, isSpider ? 'SERP on Spider Campaign' : 'SERP Campaign')));
|
|
339
|
+
children.push(Box({ flexDirection: 'row' }, Text({ color: themeColor('success') }, `🔎 Queries: ${queriesFound}/${queriesRequested} `), Text({ color: themeColor('warning') }, `⚠ blocked ${queriesBlocked} `), Text({ color: themeColor('warning') }, `🧩 captcha ${queriesCaptcha} `), Text({ color: themeColor('accent') }, `✅ hitRate ${foundRate}% `), Text({ color: themeColor('mutedForeground') }, `🏆 Top3 ${top3} `), Text({ color: themeColor('mutedForeground') }, `🏅 Top10 ${top10} `), avgTop !== null && avgTop !== undefined ? Text({ color: themeColor('foreground') }, `📈 avgPos ${Math.round(avgTop)}`) : null));
|
|
340
|
+
if (serp.campaign) {
|
|
341
|
+
const campaignConfidence = serp.campaign.confidence || 'low';
|
|
342
|
+
const campaignActive = serp.campaign.active ? 'active' : 'inactive';
|
|
343
|
+
const color = campaignConfidence === 'high' ? themeColor('success') : campaignConfidence === 'medium' ? themeColor('warning') : themeColor('mutedForeground');
|
|
344
|
+
children.push(Box({ flexDirection: 'row' }, Text({ color: campaignActive === 'active' ? themeColor('success') : themeColor('mutedForeground') }, `Campaign ${campaignActive}`), Text({ color }, ` confidence ${campaignConfidence}`)));
|
|
345
|
+
if ((serp.campaign.evidence || []).length > 0) {
|
|
346
|
+
const evidence = serp.campaign.evidence.slice(0, 2);
|
|
347
|
+
children.push(Box({ marginTop: 1 }, Text({ color: themeColor('mutedForeground') }, `Evidence: ${evidence.join(' • ')}`)));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if ((summary.competitorCoverage?.organicUniqueDomains !== undefined || summary.competitorCoverage?.paidUniqueDomains !== undefined)) {
|
|
351
|
+
children.push(Text({ color: themeColor('mutedForeground') }, `Competition: ${summary.competitorCoverage?.organicUniqueDomains || 0} organic domains, ${summary.competitorCoverage?.paidUniqueDomains || 0} paid domains`));
|
|
352
|
+
}
|
|
353
|
+
const organic = (summary.topOrganicCompetitors || []).slice(0, 6);
|
|
354
|
+
if (organic.length > 0) {
|
|
355
|
+
const barData = organic.map((item) => ({
|
|
356
|
+
label: item.domain.replace(/^www\./, '').slice(0, 20),
|
|
357
|
+
value: Math.max(0, item.matchedKeywords || item.totalOutperformedQueries || 0),
|
|
358
|
+
color: themeColor('success'),
|
|
359
|
+
}));
|
|
360
|
+
children.push(Box({ marginTop: 1 }, Text({ color: themeColor('foreground'), bold: true }, 'Main organic competitors')));
|
|
361
|
+
children.push(BarChart({
|
|
362
|
+
data: barData,
|
|
363
|
+
orientation: 'horizontal',
|
|
364
|
+
width: Math.min(45, width - 4),
|
|
365
|
+
showValues: true,
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
const resultFoundSeries = (serp.results || []).slice(0, 12).map((item) => (item.found ? 100 : 0));
|
|
369
|
+
if (resultFoundSeries.length > 1) {
|
|
370
|
+
children.push(Box({ marginTop: 1 }, Text({ color: themeColor('foreground'), bold: true }, 'SERP hit history')));
|
|
371
|
+
children.push(Box({ flexDirection: 'row' }, Sparkline({
|
|
372
|
+
data: resultFoundSeries,
|
|
373
|
+
width: Math.min(45, width - 4),
|
|
374
|
+
color: 'green',
|
|
375
|
+
max: 100,
|
|
376
|
+
min: 0,
|
|
377
|
+
}), Box({ marginLeft: 1 }, Text({ color: themeColor('foreground'), dim: true }, `${resultFoundSeries.filter(v => v === 100).length}/${resultFoundSeries.length}`))));
|
|
378
|
+
}
|
|
379
|
+
const pageRates = (serp.pageComparison || [])
|
|
380
|
+
.map((page) => ({
|
|
381
|
+
pageUrl: page.pageUrl,
|
|
382
|
+
rate: toPercent(page.appearanceRate),
|
|
383
|
+
}))
|
|
384
|
+
.slice(0, 10);
|
|
385
|
+
if (pageRates.length > 1) {
|
|
386
|
+
children.push(Box({ marginTop: 1 }, Text({ color: themeColor('foreground'), bold: true }, 'Top page hit rate')));
|
|
387
|
+
children.push(Sparkline({
|
|
388
|
+
data: pageRates.map((item) => item.rate),
|
|
389
|
+
width: Math.min(45, width - 4),
|
|
390
|
+
color: 'blue',
|
|
391
|
+
max: 100,
|
|
392
|
+
min: 0,
|
|
393
|
+
}));
|
|
394
|
+
children.push(Box({ flexDirection: 'column' }, ...pageRates
|
|
395
|
+
.slice(0, 4)
|
|
396
|
+
.map((page) => Text({ color: themeColor('mutedForeground') }, ` ${page.pageUrl.slice(0, 38)} (${page.rate}%)`))));
|
|
397
|
+
}
|
|
398
|
+
return children;
|
|
399
|
+
}
|
|
316
400
|
function renderSeoResponse(data, width) {
|
|
317
401
|
try {
|
|
318
402
|
const children = [];
|
|
@@ -515,6 +599,9 @@ function renderSeoResponse(data, width) {
|
|
|
515
599
|
children.push(renderBar('Images', comp.images || 0));
|
|
516
600
|
children.push(renderBar('Links', comp.links || 0));
|
|
517
601
|
}
|
|
602
|
+
if (data.serp) {
|
|
603
|
+
children.push(...renderSerpSection(data, contentWidth, false));
|
|
604
|
+
}
|
|
518
605
|
return Box({ flexDirection: 'column', width }, ...children);
|
|
519
606
|
}
|
|
520
607
|
catch (err) {
|
|
@@ -524,6 +611,7 @@ function renderSeoResponse(data, width) {
|
|
|
524
611
|
function renderSpiderResponse(data, width) {
|
|
525
612
|
const children = [];
|
|
526
613
|
const hasSeo = !!data.seo;
|
|
614
|
+
const contentWidth = Math.max(40, width - 4);
|
|
527
615
|
children.push(Box({ flexDirection: 'row' }, Text({ color: themeColor('primary'), bold: true }, hasSeo ? '🔍 SEO Spider Report' : '🕷️ Spider Crawl Report')));
|
|
528
616
|
if (data.url) {
|
|
529
617
|
children.push(Text({ color: themeColor('mutedForeground') }, `URL: ${data.url}`));
|
|
@@ -536,6 +624,32 @@ function renderSpiderResponse(data, width) {
|
|
|
536
624
|
if (data.assets) {
|
|
537
625
|
children.push(Box({ flexDirection: 'row' }, Text({ color: themeColor('mutedForeground') }, `🖼 Images: ${data.assets.images || 0} `), Text({ color: themeColor('mutedForeground') }, `📜 Scripts: ${data.assets.scripts || 0} `), Text({ color: themeColor('mutedForeground') }, `🎨 Stylesheets: ${data.assets.stylesheets || 0}`)));
|
|
538
626
|
}
|
|
627
|
+
if (data.security) {
|
|
628
|
+
const security = data.security;
|
|
629
|
+
const totalPages = security.pages || (data.pages || 0);
|
|
630
|
+
const blockedPages = security.blockedPages || 0;
|
|
631
|
+
const captchaPages = security.captchaPages || 0;
|
|
632
|
+
const captchaProviders = security.captchaProviders || {};
|
|
633
|
+
const transportUsage = security.transportUsage || {};
|
|
634
|
+
const undiciRequests = transportUsage.undici || 0;
|
|
635
|
+
const curlRequests = transportUsage.curl || 0;
|
|
636
|
+
const transportTotal = undiciRequests + curlRequests;
|
|
637
|
+
const blockedRate = totalPages > 0 ? Math.round((blockedPages / totalPages) * 100) : 0;
|
|
638
|
+
const blockedColor = blockedRate === 0 ? themeColor('success') : blockedRate <= 15 ? themeColor('warning') : themeColor('error');
|
|
639
|
+
const providerSummary = Object.entries(captchaProviders)
|
|
640
|
+
.map(([provider, count]) => `${provider}: ${count}`)
|
|
641
|
+
.join(', ');
|
|
642
|
+
children.push(Box({ marginTop: 1 }, Text({ color: themeColor('foreground'), bold: true }, 'Anti-Bot Signals')));
|
|
643
|
+
children.push(Box({ flexDirection: 'row' }, Text({ color: blockedColor }, `🚫 blocked ${blockedPages}/${totalPages} (${blockedRate}%) `), Text({ color: themeColor('warning') }, `🤖 captcha ${captchaPages}`), Text({ color: themeColor('mutedForeground') }, ` 🔁 attempts ${security.attempts || 0} │ retries ${security.retries || 0}`)));
|
|
644
|
+
children.push(Box({ flexDirection: 'row' }, Text({ color: themeColor('success') }, `Avg attempts: ${security.avgAttempts !== undefined ? security.avgAttempts.toFixed(2) : 'n/a'}`), Text({ color: themeColor('mutedForeground') }, ` Avg TTFB: ${security.avgTtfbMs ?? 'n/a'}ms`), Text({ color: themeColor('mutedForeground') }, ` Avg total: ${security.avgTotalMs ?? 'n/a'}ms`), Text({ color: themeColor('mutedForeground') }, ` Avg download: ${security.avgDownloadMs ?? 'n/a'}ms`)));
|
|
645
|
+
children.push(Text({ color: themeColor('mutedForeground') }, `Captcha providers: ${providerSummary || 'n/a'}`));
|
|
646
|
+
if (transportTotal > 0) {
|
|
647
|
+
children.push(Text({ color: themeColor('mutedForeground') }, `Transport split: curl ${curlRequests} (${Math.round((curlRequests / transportTotal) * 100)}%) / undici ${undiciRequests} (${Math.round((undiciRequests / transportTotal) * 100)}%)`));
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
children.push(Text({ color: themeColor('mutedForeground') }, `Transport split: not enough data`));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
539
653
|
if (data.seo) {
|
|
540
654
|
const seo = data.seo;
|
|
541
655
|
children.push(Box({ marginTop: 1 }, Text({ color: themeColor('foreground'), bold: true }, 'SEO Summary')));
|
|
@@ -592,6 +706,9 @@ function renderSpiderResponse(data, width) {
|
|
|
592
706
|
}
|
|
593
707
|
}
|
|
594
708
|
}
|
|
709
|
+
if (data.serp) {
|
|
710
|
+
children.push(...renderSerpSection(data, contentWidth, true));
|
|
711
|
+
}
|
|
595
712
|
if (data.topPages && data.topPages.length > 0) {
|
|
596
713
|
children.push(Box({ marginTop: 1 }, Text({ color: themeColor('foreground'), bold: true }, `Top ${Math.min(10, data.topPages.length)} Pages`)));
|
|
597
714
|
for (const page of data.topPages.slice(0, 10)) {
|