recker 1.0.29-next.3524ab6 → 1.0.29-next.7cc1d8b

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.
@@ -119,17 +119,17 @@ export class ScrollBuffer extends EventEmitter {
119
119
  }
120
120
  export function parseScrollKey(data) {
121
121
  const str = data.toString();
122
- if (str === '\x1b[5~' || str === '\x1bOy' || str === '\x1b[5;5~' || str === '\x1b[5;2~')
122
+ if (str === '\x1b[5~' || str === '\x1bOy')
123
123
  return 'pageUp';
124
- if (str === '\x1b[6~' || str === '\x1bOs' || str === '\x1b[6;5~' || str === '\x1b[6;2~')
124
+ if (str === '\x1b[6~' || str === '\x1bOs')
125
125
  return 'pageDown';
126
126
  if (str === '\x1b[1;2A')
127
127
  return 'scrollUp';
128
128
  if (str === '\x1b[1;2B')
129
129
  return 'scrollDown';
130
- if (str === '\x1b[H' || str === '\x1b[1~' || str === '\x1bOH' || str === '\x1b[7~')
130
+ if (str === '\x1b[H' || str === '\x1b[1~' || str === '\x1bOH')
131
131
  return 'home';
132
- if (str === '\x1b[F' || str === '\x1b[4~' || str === '\x1bOF' || str === '\x1b[8~')
132
+ if (str === '\x1b[F' || str === '\x1b[4~' || str === '\x1bOF')
133
133
  return 'end';
134
134
  if (str === 'q' || str === 'Q')
135
135
  return 'quit';
@@ -173,36 +173,20 @@ export class RekShell {
173
173
  }
174
174
  return true;
175
175
  }
176
- try {
177
- const scrollKey = parseScrollKey(data);
178
- if (scrollKey) {
179
- if (scrollKey === 'quit') {
180
- if (self.inScrollMode) {
181
- self.exitScrollMode();
182
- return true;
183
- }
184
- return originalEmit(event, ...args);
185
- }
186
- self.handleScrollKey(scrollKey);
187
- return true;
188
- }
189
- if (self.inScrollMode) {
190
- if (str === '\x1b[A') {
191
- self.handleScrollKey('scrollUp');
192
- return true;
193
- }
194
- if (str === '\x1b[B') {
195
- self.handleScrollKey('scrollDown');
196
- return true;
197
- }
198
- if (str === '\x1b' || str === '\x1b\x1b') {
176
+ const scrollKey = parseScrollKey(data);
177
+ if (scrollKey) {
178
+ if (scrollKey === 'quit') {
179
+ if (self.inScrollMode) {
199
180
  self.exitScrollMode();
200
181
  return true;
201
182
  }
202
- return true;
183
+ return originalEmit(event, ...args);
203
184
  }
185
+ self.handleScrollKey(scrollKey);
186
+ return true;
204
187
  }
205
- catch {
188
+ if (self.inScrollMode) {
189
+ return true;
206
190
  }
207
191
  }
208
192
  return originalEmit(event, ...args);
@@ -210,9 +194,6 @@ export class RekShell {
210
194
  }
211
195
  }
212
196
  handleScrollKey(key) {
213
- if (!this.originalStdoutWrite) {
214
- return;
215
- }
216
197
  let needsRedraw = false;
217
198
  switch (key) {
218
199
  case 'pageUp':
@@ -269,15 +250,11 @@ export class RekShell {
269
250
  enterScrollMode() {
270
251
  if (this.inScrollMode)
271
252
  return;
272
- if (!this.originalStdoutWrite)
273
- return;
274
253
  this.inScrollMode = true;
275
- try {
276
- this.rl.pause();
277
- }
278
- catch {
254
+ this.rl.pause();
255
+ if (this.originalStdoutWrite) {
256
+ this.originalStdoutWrite('\x1b[?25l');
279
257
  }
280
- this.originalStdoutWrite('\x1b[?25l');
281
258
  this.renderScrollView();
282
259
  }
283
260
  exitScrollMode() {
@@ -311,7 +288,7 @@ export class RekShell {
311
288
  const scrollInfo = this.scrollBuffer.isScrolledUp
312
289
  ? colors.yellow(`↑ ${this.scrollBuffer.position} lines | ${info.percent}% | `)
313
290
  : '';
314
- const helpText = colors.gray('↑↓/PgUp/PgDn • Home/End • Esc/Q to exit');
291
+ const helpText = colors.gray('Page Up/Down • Home/End • Q to exit');
315
292
  const statusBar = `\x1b[${rows};1H\x1b[7m ${scrollInfo}${helpText} \x1b[0m`;
316
293
  this.originalStdoutWrite(statusBar);
317
294
  }
@@ -8,7 +8,6 @@ import { createHybridSearch } from './search/index.js';
8
8
  import { UnsupportedError } from '../core/errors.js';
9
9
  import { getIpInfo, isValidIP, isGeoIPAvailable, isBogon, isIPv6 } from './ip-intel.js';
10
10
  import { networkTools, networkToolHandlers } from './tools/network.js';
11
- import { seoTools, seoToolHandlers } from './tools/seo.js';
12
11
  import { ToolRegistry } from './tools/registry.js';
13
12
  import { loadToolModules } from './tools/loader.js';
14
13
  export class MCPServer {
@@ -46,10 +45,6 @@ export class MCPServer {
46
45
  tools: networkTools,
47
46
  handlers: networkToolHandlers
48
47
  });
49
- this.toolRegistry.registerModule({
50
- tools: seoTools,
51
- handlers: seoToolHandlers
52
- });
53
48
  }
54
49
  indexReady = null;
55
50
  async ensureIndexReady() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recker",
3
- "version": "1.0.29-next.3524ab6",
3
+ "version": "1.0.29-next.7cc1d8b",
4
4
  "description": "AI & DevX focused HTTP client for Node.js 18+",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,3 +0,0 @@
1
- import type { MCPTool, MCPToolResult } from '../types.js';
2
- export declare const seoTools: MCPTool[];
3
- export declare const seoToolHandlers: Record<string, (args: Record<string, unknown>) => Promise<MCPToolResult>>;
@@ -1,427 +0,0 @@
1
- import { createClient } from '../../core/client.js';
2
- import { analyzeSeo } from '../../seo/analyzer.js';
3
- import { seoSpider } from '../../seo/seo-spider.js';
4
- function formatCheck(check) {
5
- const icon = check.status === 'pass' ? '✓' : check.status === 'fail' ? '✗' : '⚠';
6
- let line = `${icon} [${check.status.toUpperCase()}] ${check.name}: ${check.message}`;
7
- if (check.recommendation) {
8
- line += `\n → ${check.recommendation}`;
9
- }
10
- return line;
11
- }
12
- function createIssueSummary(checks) {
13
- const critical = checks.filter(c => c.status === 'fail');
14
- const warnings = checks.filter(c => c.status === 'warn');
15
- const passed = checks.filter(c => c.status === 'pass').length;
16
- return { critical, warnings, passed, total: checks.length };
17
- }
18
- function extractCategory(name) {
19
- const lowerName = name.toLowerCase();
20
- const categories = [
21
- 'meta', 'content', 'links', 'images', 'technical', 'security',
22
- 'performance', 'mobile', 'accessibility', 'schema', 'structural',
23
- 'i18n', 'pwa', 'social', 'ecommerce', 'local', 'cwv',
24
- 'readability', 'crawl', 'internal-linking', 'best-practices'
25
- ];
26
- for (const cat of categories) {
27
- if (lowerName.startsWith(cat) || lowerName.includes(cat)) {
28
- return cat;
29
- }
30
- }
31
- return 'general';
32
- }
33
- function generateQuickWins(report) {
34
- const quickWins = [];
35
- for (const check of report.checks) {
36
- if (check.status === 'pass')
37
- continue;
38
- const category = extractCategory(check.name);
39
- let priority = 'medium';
40
- if (check.status === 'fail') {
41
- priority = 'high';
42
- }
43
- else if (['meta', 'title', 'content'].includes(category)) {
44
- priority = 'medium';
45
- }
46
- else {
47
- priority = 'low';
48
- }
49
- if (check.name.toLowerCase().includes('title') && check.status === 'fail') {
50
- priority = 'high';
51
- }
52
- if (check.name.toLowerCase().includes('meta description') && check.status === 'fail') {
53
- priority = 'high';
54
- }
55
- if (check.name.toLowerCase().includes('h1') && check.status === 'fail') {
56
- priority = 'high';
57
- }
58
- if (check.name.toLowerCase().includes('canonical') && check.status === 'fail') {
59
- priority = 'high';
60
- }
61
- if (check.name.toLowerCase().includes('https') && check.status === 'fail') {
62
- priority = 'high';
63
- }
64
- quickWins.push({
65
- priority,
66
- category,
67
- issue: check.name,
68
- action: check.recommendation || check.message,
69
- impact: check.evidence?.impact || 'Improves SEO ranking and user experience',
70
- });
71
- }
72
- const priorityOrder = { high: 0, medium: 1, low: 2 };
73
- quickWins.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
74
- return quickWins;
75
- }
76
- async function seoAnalyze(args) {
77
- const url = String(args.url || '');
78
- const categories = args.categories;
79
- const verbose = Boolean(args.verbose);
80
- if (!url) {
81
- return {
82
- content: [{ type: 'text', text: 'Error: url is required' }],
83
- isError: true,
84
- };
85
- }
86
- try {
87
- const client = createClient({ timeout: 30000 });
88
- const response = await client.get(url);
89
- const html = await response.text();
90
- const responseHeaders = {};
91
- response.headers.forEach((val, key) => {
92
- responseHeaders[key] = val;
93
- });
94
- const report = await analyzeSeo(html, {
95
- baseUrl: url,
96
- responseHeaders,
97
- rules: categories ? { categories: categories } : undefined,
98
- });
99
- const summary = createIssueSummary(report.checks);
100
- const output = {
101
- url,
102
- score: report.score,
103
- grade: report.grade,
104
- summary: {
105
- critical: summary.critical.length,
106
- warnings: summary.warnings.length,
107
- passed: summary.passed,
108
- total: summary.total,
109
- },
110
- timing: report.timing,
111
- };
112
- if (report.openGraph) {
113
- output.openGraph = report.openGraph;
114
- }
115
- if (summary.critical.length > 0) {
116
- output.criticalIssues = summary.critical.map(c => ({
117
- name: c.name,
118
- message: c.message,
119
- recommendation: c.recommendation,
120
- evidence: c.evidence,
121
- }));
122
- }
123
- if (summary.warnings.length > 0) {
124
- output.warnings = summary.warnings.slice(0, verbose ? undefined : 10).map(c => ({
125
- name: c.name,
126
- message: c.message,
127
- recommendation: c.recommendation,
128
- }));
129
- if (!verbose && summary.warnings.length > 10) {
130
- output.warningsNote = `Showing 10 of ${summary.warnings.length} warnings. Use verbose=true to see all.`;
131
- }
132
- }
133
- if (verbose) {
134
- output.detailedAnalysis = {
135
- title: report.title,
136
- metaDescription: report.metaDescription,
137
- headings: report.headings,
138
- content: report.content,
139
- links: report.links,
140
- images: report.images,
141
- technical: report.technical,
142
- };
143
- }
144
- return {
145
- content: [{
146
- type: 'text',
147
- text: JSON.stringify(output, null, 2),
148
- }],
149
- };
150
- }
151
- catch (error) {
152
- return {
153
- content: [{
154
- type: 'text',
155
- text: `SEO analysis failed: ${error.message}`,
156
- }],
157
- isError: true,
158
- };
159
- }
160
- }
161
- async function seoSpiderCrawl(args) {
162
- const url = String(args.url || '');
163
- const maxPages = Number(args.maxPages) || 20;
164
- const maxDepth = Number(args.maxDepth) || 3;
165
- const concurrency = Number(args.concurrency) || 3;
166
- if (!url) {
167
- return {
168
- content: [{ type: 'text', text: 'Error: url is required' }],
169
- isError: true,
170
- };
171
- }
172
- try {
173
- const result = await seoSpider(url, {
174
- seo: true,
175
- maxPages,
176
- maxDepth,
177
- concurrency,
178
- delay: 200,
179
- });
180
- const output = {
181
- url,
182
- crawlDuration: result.duration,
183
- summary: result.summary,
184
- };
185
- if (result.siteWideIssues.length > 0) {
186
- output.siteWideIssues = result.siteWideIssues.map(issue => ({
187
- type: issue.type,
188
- severity: issue.severity,
189
- message: issue.message,
190
- affectedUrls: issue.affectedUrls.slice(0, 5),
191
- affectedCount: issue.affectedUrls.length,
192
- value: issue.value,
193
- }));
194
- }
195
- const pageResults = result.pages
196
- .filter(p => p.seoReport)
197
- .map(p => ({
198
- url: p.url,
199
- status: p.status,
200
- score: p.seoReport?.score,
201
- grade: p.seoReport?.grade,
202
- criticalIssues: p.seoReport?.checks.filter(c => c.status === 'fail').length || 0,
203
- warnings: p.seoReport?.checks.filter(c => c.status === 'warn').length || 0,
204
- }))
205
- .sort((a, b) => (a.score || 0) - (b.score || 0));
206
- output.pages = pageResults;
207
- const errorPages = result.pages.filter(p => p.error);
208
- if (errorPages.length > 0) {
209
- output.crawlErrors = errorPages.map(p => ({
210
- url: p.url,
211
- error: p.error,
212
- }));
213
- }
214
- const recommendations = [];
215
- if (result.summary.duplicateTitles > 0) {
216
- recommendations.push(`Fix ${result.summary.duplicateTitles} pages with duplicate titles - each page should have a unique title`);
217
- }
218
- if (result.summary.duplicateDescriptions > 0) {
219
- recommendations.push(`Fix ${result.summary.duplicateDescriptions} pages with duplicate meta descriptions`);
220
- }
221
- if (result.summary.duplicateH1s > 0) {
222
- recommendations.push(`Fix ${result.summary.duplicateH1s} pages with duplicate H1 headings`);
223
- }
224
- if (result.summary.orphanPages > 0) {
225
- recommendations.push(`Add internal links to ${result.summary.orphanPages} orphan pages that have no incoming links`);
226
- }
227
- if (result.summary.avgScore < 70) {
228
- recommendations.push(`Overall site SEO score is ${result.summary.avgScore}/100 - focus on pages with lowest scores first`);
229
- }
230
- if (recommendations.length > 0) {
231
- output.recommendations = recommendations;
232
- }
233
- return {
234
- content: [{
235
- type: 'text',
236
- text: JSON.stringify(output, null, 2),
237
- }],
238
- };
239
- }
240
- catch (error) {
241
- return {
242
- content: [{
243
- type: 'text',
244
- text: `SEO spider failed: ${error.message}`,
245
- }],
246
- isError: true,
247
- };
248
- }
249
- }
250
- async function seoQuickWins(args) {
251
- const url = String(args.url || '');
252
- const limit = Number(args.limit) || 10;
253
- if (!url) {
254
- return {
255
- content: [{ type: 'text', text: 'Error: url is required' }],
256
- isError: true,
257
- };
258
- }
259
- try {
260
- const client = createClient({ timeout: 30000 });
261
- const response = await client.get(url);
262
- const html = await response.text();
263
- const responseHeaders = {};
264
- response.headers.forEach((val, key) => {
265
- responseHeaders[key] = val;
266
- });
267
- const report = await analyzeSeo(html, {
268
- baseUrl: url,
269
- responseHeaders,
270
- });
271
- const quickWins = generateQuickWins(report).slice(0, limit);
272
- const high = quickWins.filter(w => w.priority === 'high');
273
- const medium = quickWins.filter(w => w.priority === 'medium');
274
- const low = quickWins.filter(w => w.priority === 'low');
275
- const output = {
276
- url,
277
- score: report.score,
278
- grade: report.grade,
279
- quickWins: {
280
- high: high.map(w => ({
281
- issue: w.issue,
282
- action: w.action,
283
- impact: w.impact,
284
- category: w.category,
285
- })),
286
- medium: medium.map(w => ({
287
- issue: w.issue,
288
- action: w.action,
289
- category: w.category,
290
- })),
291
- low: low.map(w => ({
292
- issue: w.issue,
293
- action: w.action,
294
- category: w.category,
295
- })),
296
- },
297
- summary: {
298
- totalIssues: quickWins.length,
299
- highPriority: high.length,
300
- mediumPriority: medium.length,
301
- lowPriority: low.length,
302
- },
303
- advice: high.length > 0
304
- ? `Start with the ${high.length} high-priority issues first. These have the biggest impact on SEO.`
305
- : medium.length > 0
306
- ? `Good job! No critical issues. Focus on the ${medium.length} medium-priority improvements.`
307
- : 'Excellent! Your page is well-optimized. Consider the minor improvements listed.',
308
- };
309
- return {
310
- content: [{
311
- type: 'text',
312
- text: JSON.stringify(output, null, 2),
313
- }],
314
- };
315
- }
316
- catch (error) {
317
- return {
318
- content: [{
319
- type: 'text',
320
- text: `Quick wins analysis failed: ${error.message}`,
321
- }],
322
- isError: true,
323
- };
324
- }
325
- }
326
- export const seoTools = [
327
- {
328
- name: 'rek_seo_analyze',
329
- description: `Analyze a single web page for SEO issues using 250+ rules across 21 categories.
330
-
331
- Returns:
332
- - SEO score (0-100) and grade (A-F)
333
- - Critical issues that must be fixed
334
- - Warnings and recommendations
335
- - OpenGraph/social meta analysis
336
- - Request timing breakdown
337
-
338
- Perfect for analyzing your localhost dev server or any public URL. Categories include: meta, content, links, images, technical, security, performance, mobile, accessibility, schema, structural, i18n, PWA, social, e-commerce, local SEO, Core Web Vitals, readability, crawlability, internal linking, and best practices.`,
339
- inputSchema: {
340
- type: 'object',
341
- properties: {
342
- url: {
343
- type: 'string',
344
- description: 'URL to analyze (e.g., http://localhost:3000 or https://example.com)',
345
- },
346
- categories: {
347
- type: 'array',
348
- items: { type: 'string' },
349
- description: 'Filter by specific categories (e.g., ["meta", "security", "performance"]). Leave empty for all.',
350
- },
351
- verbose: {
352
- type: 'boolean',
353
- description: 'Include detailed analysis (headings, links, images breakdown)',
354
- default: false,
355
- },
356
- },
357
- required: ['url'],
358
- },
359
- },
360
- {
361
- name: 'rek_seo_spider',
362
- description: `Crawl an entire website and analyze SEO across all pages.
363
-
364
- Detects site-wide issues:
365
- - Duplicate titles, descriptions, and H1s
366
- - Orphan pages (no internal links pointing to them)
367
- - Pages with low SEO scores
368
-
369
- Returns per-page scores and prioritized recommendations for improving overall site SEO. Great for auditing a full site before launch or finding issues across your dev environment.`,
370
- inputSchema: {
371
- type: 'object',
372
- properties: {
373
- url: {
374
- type: 'string',
375
- description: 'Starting URL to crawl (e.g., http://localhost:3000)',
376
- },
377
- maxPages: {
378
- type: 'number',
379
- description: 'Maximum pages to crawl',
380
- default: 20,
381
- },
382
- maxDepth: {
383
- type: 'number',
384
- description: 'Maximum link depth to follow',
385
- default: 3,
386
- },
387
- concurrency: {
388
- type: 'number',
389
- description: 'Parallel requests (be respectful to servers)',
390
- default: 3,
391
- },
392
- },
393
- required: ['url'],
394
- },
395
- },
396
- {
397
- name: 'rek_seo_quick_wins',
398
- description: `Get prioritized, actionable SEO improvements for a page.
399
-
400
- Returns issues sorted by priority (high/medium/low) with:
401
- - What to fix
402
- - How to fix it
403
- - Expected impact
404
-
405
- Use this when you want a focused list of what to work on next, rather than a full audit.`,
406
- inputSchema: {
407
- type: 'object',
408
- properties: {
409
- url: {
410
- type: 'string',
411
- description: 'URL to analyze',
412
- },
413
- limit: {
414
- type: 'number',
415
- description: 'Maximum number of quick wins to return',
416
- default: 10,
417
- },
418
- },
419
- required: ['url'],
420
- },
421
- },
422
- ];
423
- export const seoToolHandlers = {
424
- rek_seo_analyze: seoAnalyze,
425
- rek_seo_spider: seoSpiderCrawl,
426
- rek_seo_quick_wins: seoQuickWins,
427
- };