recker 1.0.75 → 1.0.76-next.c8fd7f9

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.
@@ -0,0 +1,107 @@
1
+ import type { SearchTransport } from '../search/google.js';
2
+ import type { SeoReport } from './types.js';
3
+ export type KeywordCampaignSource = 'discovered' | 'preset';
4
+ export type CampaignResultPlacement = 'ad' | 'organic' | 'unknown';
5
+ export interface KeywordCampaignSeedInput {
6
+ keyword: string;
7
+ source?: KeywordCampaignSource;
8
+ sourcePage?: string;
9
+ weight?: number;
10
+ }
11
+ export interface KeywordCampaignSeed {
12
+ keyword: string;
13
+ normalizedKeyword: string;
14
+ source: KeywordCampaignSource;
15
+ sourcePage?: string;
16
+ weight: number;
17
+ }
18
+ export interface KeywordCampaignOptions {
19
+ targetUrl: string;
20
+ discoveredKeywords?: KeywordCampaignSeedInput[] | string[];
21
+ presetKeywords?: string[];
22
+ minKeywordLength?: number;
23
+ maxQueries?: number;
24
+ maxResultsPerQuery?: number;
25
+ transport?: SearchTransport;
26
+ timeout?: number;
27
+ country?: string;
28
+ gl?: string;
29
+ hl?: string;
30
+ minWeight?: number;
31
+ }
32
+ export interface KeywordCampaignCompetitorResult {
33
+ domain: string;
34
+ rank: number;
35
+ placement: CampaignResultPlacement;
36
+ placementHint?: string;
37
+ url: string;
38
+ title: string;
39
+ }
40
+ export interface KeywordCampaignResult {
41
+ keyword: string;
42
+ source: KeywordCampaignSource;
43
+ sourcePage?: string;
44
+ sourceWeight: number;
45
+ found: boolean;
46
+ bestPosition: number | null;
47
+ totalChecked: number;
48
+ matchedUrl?: string;
49
+ matchedTitle?: string;
50
+ matchedDisplayUrl?: string;
51
+ placement: CampaignResultPlacement;
52
+ placementHint?: string;
53
+ searchUrl: string;
54
+ searchTransport: SearchTransport;
55
+ competitors: KeywordCampaignCompetitorResult[];
56
+ }
57
+ export interface KeywordCampaignPageStats {
58
+ pageUrl: string;
59
+ tracked: number;
60
+ found: number;
61
+ avgPosition: number | null;
62
+ top3: number;
63
+ top10: number;
64
+ }
65
+ export interface KeywordCampaignSummary {
66
+ queriesRequested: number;
67
+ queriesExecuted: number;
68
+ queriesFound: number;
69
+ avgTopPosition: number | null;
70
+ top3Count: number;
71
+ top10Count: 0 | number;
72
+ topOrganicCompetitors: KeywordCampaignCompetitorSummary[];
73
+ topPaidCompetitors: KeywordCampaignCompetitorSummary[];
74
+ competitorCoverage: {
75
+ organicUniqueDomains: number;
76
+ paidUniqueDomains: number;
77
+ };
78
+ }
79
+ export interface KeywordCampaignCompetitorSummary {
80
+ domain: string;
81
+ organicQueries: number;
82
+ paidQueries: number;
83
+ bestOrganicRank: number | null;
84
+ bestPaidRank: number | null;
85
+ matchedKeywords: number;
86
+ }
87
+ export interface CampaignActivitySignal {
88
+ active: boolean;
89
+ confidence: 'high' | 'medium' | 'low';
90
+ evidence: string[];
91
+ }
92
+ export interface KeywordCampaignReport {
93
+ targetUrl: string;
94
+ targetDomain: string;
95
+ results: KeywordCampaignResult[];
96
+ summary: KeywordCampaignSummary;
97
+ pageComparison: KeywordCampaignPageStats[];
98
+ campaign: CampaignActivitySignal;
99
+ }
100
+ export interface KeywordCampaignExtractionOptions {
101
+ maxKeywords?: number;
102
+ minKeywordLength?: number;
103
+ }
104
+ export declare function extractKeywordCampaignSeedsFromReport(report: SeoReport, options?: KeywordCampaignExtractionOptions & {
105
+ sourcePage?: string;
106
+ }): KeywordCampaignSeed[];
107
+ export declare function analyzeKeywordCampaign(options: KeywordCampaignOptions): Promise<KeywordCampaignReport>;
@@ -0,0 +1,380 @@
1
+ import { searchGoogleAdvanced } from '../search/google.js';
2
+ function normalizeKeyword(value) {
3
+ return value
4
+ .toLowerCase()
5
+ .replace(/[^\p{L}\p{N}\s-]+/gu, ' ')
6
+ .replace(/\s+/g, ' ')
7
+ .trim();
8
+ }
9
+ function normalizeHost(target) {
10
+ try {
11
+ const parsed = new URL(target);
12
+ return parsed.hostname.replace(/^www\./, '').toLowerCase();
13
+ }
14
+ catch {
15
+ return target.replace(/^www\./, '').toLowerCase();
16
+ }
17
+ }
18
+ function clampNumber(value, fallback) {
19
+ if (value === undefined || !Number.isFinite(value) || value <= 0)
20
+ return fallback;
21
+ return Math.max(1, Math.floor(value));
22
+ }
23
+ function normalizePlacement(value) {
24
+ if (value === 'ad')
25
+ return 'ad';
26
+ if (value === 'organic')
27
+ return 'organic';
28
+ return 'unknown';
29
+ }
30
+ function toSeedArray(seed = []) {
31
+ return seed.map((item) => {
32
+ if (typeof item === 'string') {
33
+ return {
34
+ keyword: item,
35
+ normalizedKeyword: normalizeKeyword(item),
36
+ source: 'preset',
37
+ weight: 1,
38
+ };
39
+ }
40
+ return {
41
+ keyword: item.keyword,
42
+ normalizedKeyword: normalizeKeyword(item.keyword),
43
+ source: item.source ?? 'discovered',
44
+ sourcePage: item.sourcePage,
45
+ weight: item.weight ?? 1,
46
+ };
47
+ });
48
+ }
49
+ export function extractKeywordCampaignSeedsFromReport(report, options = {}) {
50
+ const maxKeywords = options.maxKeywords ?? 10;
51
+ const minKeywordLength = options.minKeywordLength ?? 2;
52
+ return (report.keywords?.topKeywords ?? [])
53
+ .map((item) => ({
54
+ keyword: item.word,
55
+ normalizedKeyword: normalizeKeyword(item.word),
56
+ source: 'discovered',
57
+ sourcePage: options.sourcePage,
58
+ weight: Math.max(1, Math.round(item.count)),
59
+ }))
60
+ .filter(seed => seed.keyword.trim().length >= minKeywordLength
61
+ && seed.normalizedKeyword.length >= minKeywordLength
62
+ && seed.weight > 0)
63
+ .slice(0, maxKeywords);
64
+ }
65
+ function sortSeedsByWeight(a, b) {
66
+ if (a.weight === b.weight) {
67
+ return a.normalizedKeyword.localeCompare(b.normalizedKeyword);
68
+ }
69
+ return b.weight - a.weight;
70
+ }
71
+ function mergeKeywordSeeds(discovered, preset, minKeywordLength = 2, minWeight = 1) {
72
+ const merged = new Map();
73
+ const upsert = (seed) => {
74
+ if (!seed.normalizedKeyword || seed.normalizedKeyword.length < minKeywordLength)
75
+ return;
76
+ if (seed.weight < minWeight)
77
+ return;
78
+ const existing = merged.get(seed.normalizedKeyword);
79
+ if (!existing) {
80
+ merged.set(seed.normalizedKeyword, seed);
81
+ return;
82
+ }
83
+ if (existing.source === 'preset' && seed.source === 'discovered') {
84
+ merged.set(seed.normalizedKeyword, seed);
85
+ return;
86
+ }
87
+ if (seed.source === existing.source && seed.weight > existing.weight) {
88
+ merged.set(seed.normalizedKeyword, seed);
89
+ }
90
+ };
91
+ for (const seed of discovered)
92
+ upsert(seed);
93
+ for (const seed of preset)
94
+ upsert(seed);
95
+ return [...merged.values()].sort(sortSeedsByWeight);
96
+ }
97
+ function buildEmptyReport(targetUrl, targetDomain) {
98
+ return {
99
+ targetUrl,
100
+ targetDomain,
101
+ results: [],
102
+ summary: {
103
+ queriesRequested: 0,
104
+ queriesExecuted: 0,
105
+ queriesFound: 0,
106
+ avgTopPosition: null,
107
+ top3Count: 0,
108
+ top10Count: 0,
109
+ topOrganicCompetitors: [],
110
+ topPaidCompetitors: [],
111
+ competitorCoverage: {
112
+ organicUniqueDomains: 0,
113
+ paidUniqueDomains: 0,
114
+ },
115
+ },
116
+ pageComparison: [],
117
+ campaign: {
118
+ active: false,
119
+ confidence: 'low',
120
+ evidence: [],
121
+ },
122
+ };
123
+ }
124
+ function dedupeDomainResults(results, targetDomain) {
125
+ const seen = new Set();
126
+ const domainResults = [];
127
+ for (const result of results) {
128
+ if (!result.domain || result.domain === targetDomain)
129
+ continue;
130
+ if (seen.has(result.domain))
131
+ continue;
132
+ seen.add(result.domain);
133
+ domainResults.push({
134
+ domain: result.domain,
135
+ rank: result.rank,
136
+ placement: result.placement,
137
+ placementHint: result.placementHint,
138
+ url: result.url,
139
+ title: result.title,
140
+ });
141
+ }
142
+ return domainResults;
143
+ }
144
+ function trackCompetitors(candidates, keyword, targetDomain, trackers) {
145
+ for (const competitor of candidates) {
146
+ if (competitor.domain === targetDomain)
147
+ continue;
148
+ const current = trackers.get(competitor.domain) ?? {
149
+ organicQueries: 0,
150
+ paidQueries: 0,
151
+ bestOrganicRank: null,
152
+ bestPaidRank: null,
153
+ keywords: new Set(),
154
+ };
155
+ if (competitor.placement === 'ad') {
156
+ current.paidQueries += 1;
157
+ if (current.bestPaidRank === null || competitor.rank < current.bestPaidRank) {
158
+ current.bestPaidRank = competitor.rank;
159
+ }
160
+ }
161
+ else {
162
+ current.organicQueries += 1;
163
+ if (current.bestOrganicRank === null || competitor.rank < current.bestOrganicRank) {
164
+ current.bestOrganicRank = competitor.rank;
165
+ }
166
+ }
167
+ current.keywords.add(keyword);
168
+ trackers.set(competitor.domain, current);
169
+ }
170
+ }
171
+ function finalizeCompetitorSummary(trackers) {
172
+ const ranked = [...trackers.entries()].map(([domain, tracker]) => ({
173
+ domain,
174
+ organicQueries: tracker.organicQueries,
175
+ paidQueries: tracker.paidQueries,
176
+ bestOrganicRank: tracker.bestOrganicRank,
177
+ bestPaidRank: tracker.bestPaidRank,
178
+ matchedKeywords: tracker.keywords.size,
179
+ score: (tracker.organicQueries * 2) + (tracker.paidQueries * 3),
180
+ }));
181
+ const scoreSort = (a, b) => b.score - a.score || a.domain.localeCompare(b.domain);
182
+ const topOrganic = ranked
183
+ .filter((entry) => entry.organicQueries > 0)
184
+ .sort(scoreSort)
185
+ .map(({ score: _ignored, ...entry }) => entry)
186
+ .slice(0, 10);
187
+ const topPaid = ranked
188
+ .filter((entry) => entry.paidQueries > 0)
189
+ .sort(scoreSort)
190
+ .map(({ score: _ignored, ...entry }) => entry)
191
+ .slice(0, 10);
192
+ return {
193
+ topOrganic,
194
+ topPaid,
195
+ coverage: {
196
+ organicUniqueDomains: ranked.filter((entry) => entry.organicQueries > 0).length,
197
+ paidUniqueDomains: ranked.filter((entry) => entry.paidQueries > 0).length,
198
+ },
199
+ };
200
+ }
201
+ export async function analyzeKeywordCampaign(options) {
202
+ const targetUrl = options.targetUrl.trim();
203
+ const targetDomain = normalizeHost(targetUrl);
204
+ const queryLimit = clampNumber(options.maxQueries, 20);
205
+ const defaultCampaignResultLimit = 25;
206
+ const searchResultLimit = Math.max(clampNumber(options.maxResultsPerQuery, defaultCampaignResultLimit), 20);
207
+ const minKeywordLength = options.minKeywordLength ?? 2;
208
+ const minWeight = Math.max(1, options.minWeight ?? 1);
209
+ const discovered = toSeedArray(options.discoveredKeywords ?? []);
210
+ const preset = (options.presetKeywords ?? []).map((keyword) => ({
211
+ keyword,
212
+ normalizedKeyword: normalizeKeyword(keyword),
213
+ source: 'preset',
214
+ weight: 1,
215
+ }));
216
+ const seeds = mergeKeywordSeeds(discovered, preset, minKeywordLength, minWeight).slice(0, queryLimit);
217
+ const report = buildEmptyReport(targetUrl, targetDomain);
218
+ if (seeds.length === 0 || !targetDomain) {
219
+ report.summary.queriesRequested = 0;
220
+ return report;
221
+ }
222
+ const pageBuckets = new Map();
223
+ const competitorTrackers = new Map();
224
+ const campaignEvidence = [];
225
+ report.summary.queriesRequested = seeds.length;
226
+ for (const seed of seeds) {
227
+ const query = seed.keyword;
228
+ try {
229
+ const response = await searchGoogleAdvanced(query, {
230
+ num: searchResultLimit,
231
+ transport: options.transport,
232
+ timeout: options.timeout,
233
+ country: options.country,
234
+ gl: options.gl,
235
+ hl: options.hl,
236
+ });
237
+ report.summary.queriesExecuted += 1;
238
+ const parsedResults = response.results.map((result) => ({
239
+ ...result,
240
+ domain: normalizeHost(result.url),
241
+ placement: normalizePlacement(result.placement),
242
+ }));
243
+ const matched = parsedResults.find((result) => {
244
+ return result.domain && result.domain === targetDomain;
245
+ });
246
+ const competitorCandidates = dedupeDomainResults(parsedResults.map((result) => ({
247
+ domain: result.domain,
248
+ rank: result.rank,
249
+ placement: result.placement,
250
+ placementHint: result.placementHint,
251
+ url: result.url,
252
+ title: result.title,
253
+ })), targetDomain);
254
+ trackCompetitors(competitorCandidates, seed.normalizedKeyword, targetDomain, competitorTrackers);
255
+ const searchResult = {
256
+ keyword: query,
257
+ source: seed.source,
258
+ sourcePage: seed.sourcePage,
259
+ sourceWeight: seed.weight,
260
+ found: Boolean(matched),
261
+ bestPosition: matched?.rank ?? null,
262
+ totalChecked: parsedResults.length,
263
+ matchedUrl: matched?.url,
264
+ matchedTitle: matched?.title,
265
+ matchedDisplayUrl: matched?.displayedUrl,
266
+ placement: matched ? normalizePlacement(matched.placement) : 'unknown',
267
+ placementHint: matched?.placementHint,
268
+ searchUrl: response.searchUrl,
269
+ searchTransport: response.transport.used,
270
+ competitors: competitorCandidates,
271
+ };
272
+ if (searchResult.found && searchResult.bestPosition !== null) {
273
+ const position = searchResult.bestPosition;
274
+ report.summary.queriesFound += 1;
275
+ if (position <= 3)
276
+ report.summary.top3Count += 1;
277
+ if (position <= 10)
278
+ report.summary.top10Count += 1;
279
+ if (searchResult.placement === 'ad') {
280
+ campaignEvidence.push(`${query} aparece como anúncio em posição #${position}`);
281
+ }
282
+ else {
283
+ campaignEvidence.push(`${query} aparece orgânico em posição #${position}`);
284
+ }
285
+ const pageKey = seed.sourcePage ?? (seed.source === 'preset' ? 'preset-queries' : 'unknown');
286
+ const bucket = pageBuckets.get(pageKey) ?? {
287
+ tracked: 0,
288
+ found: 0,
289
+ totalPosition: 0,
290
+ positions: [],
291
+ top3: 0,
292
+ top10: 0,
293
+ };
294
+ bucket.tracked += 1;
295
+ bucket.found += 1;
296
+ bucket.totalPosition += position;
297
+ bucket.positions.push(position);
298
+ if (position <= 3)
299
+ bucket.top3 += 1;
300
+ if (position <= 10)
301
+ bucket.top10 += 1;
302
+ pageBuckets.set(pageKey, bucket);
303
+ }
304
+ else {
305
+ const pageKey = seed.sourcePage ?? (seed.source === 'preset' ? 'preset-queries' : 'unknown');
306
+ const bucket = pageBuckets.get(pageKey) ?? {
307
+ tracked: 0,
308
+ found: 0,
309
+ totalPosition: 0,
310
+ positions: [],
311
+ top3: 0,
312
+ top10: 0,
313
+ };
314
+ bucket.tracked += 1;
315
+ pageBuckets.set(pageKey, bucket);
316
+ }
317
+ report.results.push(searchResult);
318
+ }
319
+ catch {
320
+ const pageKey = seed.sourcePage ?? (seed.source === 'preset' ? 'preset-queries' : 'unknown');
321
+ const bucket = pageBuckets.get(pageKey) ?? {
322
+ tracked: 0,
323
+ found: 0,
324
+ totalPosition: 0,
325
+ positions: [],
326
+ top3: 0,
327
+ top10: 0,
328
+ };
329
+ bucket.tracked += 1;
330
+ pageBuckets.set(pageKey, bucket);
331
+ report.summary.queriesExecuted += 1;
332
+ report.results.push({
333
+ keyword: seed.keyword,
334
+ source: seed.source,
335
+ sourcePage: seed.sourcePage,
336
+ sourceWeight: seed.weight,
337
+ found: false,
338
+ bestPosition: null,
339
+ totalChecked: searchResultLimit,
340
+ placement: 'unknown',
341
+ searchUrl: '',
342
+ searchTransport: options.transport ?? 'undici',
343
+ competitors: [],
344
+ });
345
+ campaignEvidence.push(`Falha ao buscar keyword "${seed.keyword}"`);
346
+ }
347
+ }
348
+ const foundPositions = report.results
349
+ .filter((entry) => entry.found && entry.bestPosition !== null)
350
+ .map((entry) => entry.bestPosition);
351
+ if (foundPositions.length > 0) {
352
+ report.summary.avgTopPosition = Math.round(foundPositions.reduce((acc, pos) => acc + pos, 0) / foundPositions.length);
353
+ }
354
+ const hasTop3 = foundPositions.some((position) => position <= 3);
355
+ const hasTop10 = foundPositions.some((position) => position <= 10);
356
+ report.campaign = {
357
+ active: foundPositions.some((position) => position <= 10),
358
+ confidence: hasTop3 ? 'high' : hasTop10 ? 'medium' : foundPositions.length > 0 ? 'low' : 'low',
359
+ evidence: campaignEvidence,
360
+ };
361
+ const competitorSummary = finalizeCompetitorSummary(competitorTrackers);
362
+ report.summary.topOrganicCompetitors = competitorSummary.topOrganic;
363
+ report.summary.topPaidCompetitors = competitorSummary.topPaid;
364
+ report.summary.competitorCoverage = competitorSummary.coverage;
365
+ report.pageComparison = [...pageBuckets.entries()].map(([pageUrl, bucket]) => ({
366
+ pageUrl,
367
+ tracked: bucket.tracked,
368
+ found: bucket.found,
369
+ avgPosition: bucket.found > 0 ? Math.round(bucket.totalPosition / bucket.found) : null,
370
+ top3: bucket.top3,
371
+ top10: bucket.top10,
372
+ })).sort((a, b) => {
373
+ if (a.avgPosition === null)
374
+ return 1;
375
+ if (b.avgPosition === null)
376
+ return -1;
377
+ return a.avgPosition - b.avgPosition;
378
+ });
379
+ return report;
380
+ }
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const VERSION = '1.0.75';
1
+ const VERSION = '1.0.76-next.c8fd7f9';
2
2
  let _version = null;
3
3
  export async function getVersion() {
4
4
  if (_version)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recker",
3
- "version": "1.0.75",
3
+ "version": "1.0.76-next.c8fd7f9",
4
4
  "description": "Multi-Protocol SDK for the AI Era - HTTP, WebSocket, DNS, FTP, SFTP, Telnet, HLS unified with AI providers and MCP tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",