recker 1.0.26 → 1.0.27-next.8396df6

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.
Files changed (181) hide show
  1. package/dist/browser/browser/cache.d.ts +40 -0
  2. package/dist/browser/browser/cache.js +199 -0
  3. package/dist/browser/browser/crypto.d.ts +24 -0
  4. package/dist/browser/browser/crypto.js +80 -0
  5. package/dist/browser/browser/index.d.ts +31 -0
  6. package/dist/browser/browser/index.js +31 -0
  7. package/dist/browser/browser/recker.d.ts +26 -0
  8. package/dist/browser/browser/recker.js +61 -0
  9. package/dist/browser/cache/basic-file-storage.d.ts +12 -0
  10. package/dist/browser/cache/basic-file-storage.js +50 -0
  11. package/dist/browser/cache/memory-limits.d.ts +20 -0
  12. package/dist/browser/cache/memory-limits.js +96 -0
  13. package/dist/browser/cache/memory-storage.d.ts +132 -0
  14. package/dist/browser/cache/memory-storage.js +454 -0
  15. package/dist/browser/cache.d.ts +40 -0
  16. package/dist/browser/cache.js +199 -0
  17. package/dist/browser/constants/http-status.d.ts +73 -0
  18. package/dist/browser/constants/http-status.js +156 -0
  19. package/dist/browser/cookies/memory-cookie-jar.d.ts +30 -0
  20. package/dist/browser/cookies/memory-cookie-jar.js +210 -0
  21. package/dist/browser/core/client.d.ts +118 -0
  22. package/dist/browser/core/client.js +667 -0
  23. package/dist/browser/core/errors.d.ts +142 -0
  24. package/dist/browser/core/errors.js +308 -0
  25. package/dist/browser/core/index.d.ts +5 -0
  26. package/dist/browser/core/index.js +5 -0
  27. package/dist/browser/core/request-promise.d.ts +23 -0
  28. package/dist/browser/core/request-promise.js +82 -0
  29. package/dist/browser/core/request.d.ts +20 -0
  30. package/dist/browser/core/request.js +76 -0
  31. package/dist/browser/core/response.d.ts +34 -0
  32. package/dist/browser/core/response.js +178 -0
  33. package/dist/browser/crypto.d.ts +24 -0
  34. package/dist/browser/crypto.js +80 -0
  35. package/dist/browser/index.d.ts +31 -0
  36. package/dist/browser/index.js +31 -0
  37. package/dist/browser/plugins/auth/api-key.d.ts +8 -0
  38. package/dist/browser/plugins/auth/api-key.js +27 -0
  39. package/dist/browser/plugins/auth/auth0.d.ts +33 -0
  40. package/dist/browser/plugins/auth/auth0.js +94 -0
  41. package/dist/browser/plugins/auth/aws-sigv4.d.ts +10 -0
  42. package/dist/browser/plugins/auth/aws-sigv4.js +88 -0
  43. package/dist/browser/plugins/auth/azure-ad.d.ts +48 -0
  44. package/dist/browser/plugins/auth/azure-ad.js +152 -0
  45. package/dist/browser/plugins/auth/basic.d.ts +7 -0
  46. package/dist/browser/plugins/auth/basic.js +13 -0
  47. package/dist/browser/plugins/auth/bearer.d.ts +8 -0
  48. package/dist/browser/plugins/auth/bearer.js +17 -0
  49. package/dist/browser/plugins/auth/cognito.d.ts +45 -0
  50. package/dist/browser/plugins/auth/cognito.js +208 -0
  51. package/dist/browser/plugins/auth/digest.d.ts +8 -0
  52. package/dist/browser/plugins/auth/digest.js +100 -0
  53. package/dist/browser/plugins/auth/firebase.d.ts +32 -0
  54. package/dist/browser/plugins/auth/firebase.js +195 -0
  55. package/dist/browser/plugins/auth/github-app.d.ts +36 -0
  56. package/dist/browser/plugins/auth/github-app.js +170 -0
  57. package/dist/browser/plugins/auth/google-service-account.d.ts +49 -0
  58. package/dist/browser/plugins/auth/google-service-account.js +172 -0
  59. package/dist/browser/plugins/auth/index.d.ts +15 -0
  60. package/dist/browser/plugins/auth/index.js +15 -0
  61. package/dist/browser/plugins/auth/mtls.d.ts +37 -0
  62. package/dist/browser/plugins/auth/mtls.js +140 -0
  63. package/dist/browser/plugins/auth/oauth2.d.ts +8 -0
  64. package/dist/browser/plugins/auth/oauth2.js +26 -0
  65. package/dist/browser/plugins/auth/oidc.d.ts +55 -0
  66. package/dist/browser/plugins/auth/oidc.js +222 -0
  67. package/dist/browser/plugins/auth/okta.d.ts +47 -0
  68. package/dist/browser/plugins/auth/okta.js +157 -0
  69. package/dist/browser/plugins/auth.d.ts +1 -0
  70. package/dist/browser/plugins/auth.js +1 -0
  71. package/dist/browser/plugins/cache.d.ts +15 -0
  72. package/dist/browser/plugins/cache.js +486 -0
  73. package/dist/browser/plugins/circuit-breaker.d.ts +13 -0
  74. package/dist/browser/plugins/circuit-breaker.js +100 -0
  75. package/dist/browser/plugins/compression.d.ts +4 -0
  76. package/dist/browser/plugins/compression.js +130 -0
  77. package/dist/browser/plugins/cookie-jar.d.ts +5 -0
  78. package/dist/browser/plugins/cookie-jar.js +72 -0
  79. package/dist/browser/plugins/dedup.d.ts +5 -0
  80. package/dist/browser/plugins/dedup.js +35 -0
  81. package/dist/browser/plugins/graphql.d.ts +13 -0
  82. package/dist/browser/plugins/graphql.js +58 -0
  83. package/dist/browser/plugins/grpc-web.d.ts +79 -0
  84. package/dist/browser/plugins/grpc-web.js +261 -0
  85. package/dist/browser/plugins/hls.d.ts +105 -0
  86. package/dist/browser/plugins/hls.js +395 -0
  87. package/dist/browser/plugins/jsonrpc.d.ts +75 -0
  88. package/dist/browser/plugins/jsonrpc.js +143 -0
  89. package/dist/browser/plugins/logger.d.ts +13 -0
  90. package/dist/browser/plugins/logger.js +108 -0
  91. package/dist/browser/plugins/odata.d.ts +181 -0
  92. package/dist/browser/plugins/odata.js +564 -0
  93. package/dist/browser/plugins/pagination.d.ts +16 -0
  94. package/dist/browser/plugins/pagination.js +105 -0
  95. package/dist/browser/plugins/rate-limit.d.ts +15 -0
  96. package/dist/browser/plugins/rate-limit.js +162 -0
  97. package/dist/browser/plugins/retry.d.ts +14 -0
  98. package/dist/browser/plugins/retry.js +116 -0
  99. package/dist/browser/plugins/scrape.d.ts +21 -0
  100. package/dist/browser/plugins/scrape.js +82 -0
  101. package/dist/browser/plugins/server-timing.d.ts +7 -0
  102. package/dist/browser/plugins/server-timing.js +24 -0
  103. package/dist/browser/plugins/soap.d.ts +72 -0
  104. package/dist/browser/plugins/soap.js +347 -0
  105. package/dist/browser/plugins/xml.d.ts +9 -0
  106. package/dist/browser/plugins/xml.js +194 -0
  107. package/dist/browser/plugins/xsrf.d.ts +9 -0
  108. package/dist/browser/plugins/xsrf.js +48 -0
  109. package/dist/browser/recker.d.ts +26 -0
  110. package/dist/browser/recker.js +61 -0
  111. package/dist/browser/runner/request-runner.d.ts +46 -0
  112. package/dist/browser/runner/request-runner.js +89 -0
  113. package/dist/browser/scrape/document.d.ts +44 -0
  114. package/dist/browser/scrape/document.js +210 -0
  115. package/dist/browser/scrape/element.d.ts +49 -0
  116. package/dist/browser/scrape/element.js +176 -0
  117. package/dist/browser/scrape/extractors.d.ts +16 -0
  118. package/dist/browser/scrape/extractors.js +356 -0
  119. package/dist/browser/scrape/types.d.ts +107 -0
  120. package/dist/browser/scrape/types.js +1 -0
  121. package/dist/browser/transport/fetch.d.ts +11 -0
  122. package/dist/browser/transport/fetch.js +143 -0
  123. package/dist/browser/transport/undici.d.ts +38 -0
  124. package/dist/browser/transport/undici.js +897 -0
  125. package/dist/browser/types/ai.d.ts +267 -0
  126. package/dist/browser/types/ai.js +1 -0
  127. package/dist/browser/types/index.d.ts +351 -0
  128. package/dist/browser/types/index.js +1 -0
  129. package/dist/browser/types/logger.d.ts +16 -0
  130. package/dist/browser/types/logger.js +66 -0
  131. package/dist/browser/types/udp.d.ts +138 -0
  132. package/dist/browser/types/udp.js +1 -0
  133. package/dist/browser/utils/agent-manager.d.ts +29 -0
  134. package/dist/browser/utils/agent-manager.js +160 -0
  135. package/dist/browser/utils/body.d.ts +10 -0
  136. package/dist/browser/utils/body.js +148 -0
  137. package/dist/browser/utils/charset.d.ts +15 -0
  138. package/dist/browser/utils/charset.js +169 -0
  139. package/dist/browser/utils/concurrency.d.ts +20 -0
  140. package/dist/browser/utils/concurrency.js +120 -0
  141. package/dist/browser/utils/dns.d.ts +6 -0
  142. package/dist/browser/utils/dns.js +26 -0
  143. package/dist/browser/utils/header-parser.d.ts +94 -0
  144. package/dist/browser/utils/header-parser.js +617 -0
  145. package/dist/browser/utils/html-cleaner.d.ts +1 -0
  146. package/dist/browser/utils/html-cleaner.js +21 -0
  147. package/dist/browser/utils/link-header.d.ts +69 -0
  148. package/dist/browser/utils/link-header.js +190 -0
  149. package/dist/browser/utils/optional-require.d.ts +19 -0
  150. package/dist/browser/utils/optional-require.js +105 -0
  151. package/dist/browser/utils/progress.d.ts +8 -0
  152. package/dist/browser/utils/progress.js +82 -0
  153. package/dist/browser/utils/request-pool.d.ts +22 -0
  154. package/dist/browser/utils/request-pool.js +101 -0
  155. package/dist/browser/utils/sse.d.ts +7 -0
  156. package/dist/browser/utils/sse.js +67 -0
  157. package/dist/browser/utils/streaming.d.ts +17 -0
  158. package/dist/browser/utils/streaming.js +84 -0
  159. package/dist/browser/utils/try-fn.d.ts +3 -0
  160. package/dist/browser/utils/try-fn.js +59 -0
  161. package/dist/browser/utils/user-agent.d.ts +44 -0
  162. package/dist/browser/utils/user-agent.js +100 -0
  163. package/dist/browser/utils/whois.d.ts +32 -0
  164. package/dist/browser/utils/whois.js +246 -0
  165. package/dist/browser/websocket/client.d.ts +65 -0
  166. package/dist/browser/websocket/client.js +313 -0
  167. package/dist/cli/index.d.ts +1 -0
  168. package/dist/cli/index.js +99 -3
  169. package/dist/index.d.ts +1 -0
  170. package/dist/index.js +1 -0
  171. package/dist/seo/analyzer.d.ts +20 -0
  172. package/dist/seo/analyzer.js +544 -0
  173. package/dist/seo/index.d.ts +2 -0
  174. package/dist/seo/index.js +1 -0
  175. package/dist/seo/types.d.ts +100 -0
  176. package/dist/seo/types.js +1 -0
  177. package/dist/transport/fetch.d.ts +7 -1
  178. package/dist/transport/fetch.js +58 -76
  179. package/dist/utils/columns.d.ts +14 -0
  180. package/dist/utils/columns.js +69 -0
  181. package/package.json +34 -2
@@ -0,0 +1,313 @@
1
+ import { WebSocket } from 'undici';
2
+ import { EventEmitter } from 'events';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { webToNodeStream } from '../utils/streaming.js';
5
+ import { StateError, StreamError, ConnectionError } from '../core/errors.js';
6
+ export class ReckerWebSocket extends EventEmitter {
7
+ ws = null;
8
+ url;
9
+ options;
10
+ reconnectAttempts = 0;
11
+ reconnectTimer;
12
+ heartbeatTimer;
13
+ isClosed = false;
14
+ isReconnecting = false;
15
+ pongWatchdog;
16
+ backoff;
17
+ closedByUser = false;
18
+ constructor(url, options = {}) {
19
+ super();
20
+ this.url = url;
21
+ this.options = {
22
+ protocols: options.protocols || [],
23
+ headers: options.headers || {},
24
+ reconnect: options.reconnect ?? false,
25
+ reconnectDelay: options.reconnectDelay ?? 1000,
26
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 5,
27
+ heartbeatInterval: options.heartbeatInterval ?? 30000,
28
+ heartbeatTimeout: options.heartbeatTimeout ?? 10000,
29
+ dispatcher: options.dispatcher,
30
+ proxy: options.proxy,
31
+ tls: options.tls,
32
+ perMessageDeflate: options.perMessageDeflate ?? false
33
+ };
34
+ this.backoff = {
35
+ base: this.options.reconnectDelay,
36
+ factor: 2,
37
+ jitter: true,
38
+ max: 30000
39
+ };
40
+ }
41
+ async connect() {
42
+ return new Promise((resolve, reject) => {
43
+ try {
44
+ const wsOptions = {
45
+ headers: this.options.headers,
46
+ dispatcher: this.options.dispatcher,
47
+ perMessageDeflate: this.options.perMessageDeflate,
48
+ };
49
+ if (this.options.proxy) {
50
+ const proxyConfig = typeof this.options.proxy === 'string'
51
+ ? { url: this.options.proxy }
52
+ : this.options.proxy;
53
+ const { ProxyAgent } = require('undici');
54
+ wsOptions.dispatcher = new ProxyAgent(proxyConfig.url);
55
+ }
56
+ if (this.options.tls) {
57
+ wsOptions.tls = this.options.tls;
58
+ }
59
+ this.ws = new WebSocket(this.url, this.options.protocols, wsOptions);
60
+ this.ws.addEventListener('open', () => {
61
+ this.reconnectAttempts = 0;
62
+ this.isReconnecting = false;
63
+ this.startHeartbeat();
64
+ this.emit('open');
65
+ resolve();
66
+ });
67
+ this.ws.addEventListener('message', (event) => {
68
+ const message = {
69
+ data: event.data,
70
+ isBinary: event.data instanceof Buffer
71
+ };
72
+ this.emit('message', message);
73
+ this.stopPongWatchdog();
74
+ });
75
+ this.ws.addEventListener('close', (event) => {
76
+ this.stopHeartbeat();
77
+ this.stopPongWatchdog();
78
+ this.emit('close', event.code, event.reason);
79
+ if (!this.closedByUser && !this.isClosed && this.options.reconnect) {
80
+ this.attemptReconnect();
81
+ }
82
+ });
83
+ this.ws.addEventListener('error', (event) => {
84
+ const err = event.error instanceof Error
85
+ ? event.error
86
+ : new ConnectionError('WebSocket connection error', {
87
+ host: this.url,
88
+ retriable: true,
89
+ });
90
+ this.emit('error', err);
91
+ reject(err);
92
+ });
93
+ }
94
+ catch (error) {
95
+ reject(error);
96
+ }
97
+ });
98
+ }
99
+ async send(data, options) {
100
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
101
+ throw new StateError('WebSocket is not connected', {
102
+ expectedState: 'open',
103
+ actualState: this.ws ? 'closed' : 'not-created',
104
+ });
105
+ }
106
+ const awaitDrain = options?.awaitDrain ?? false;
107
+ const highWaterMark = options?.highWaterMark ?? 16 * 1024;
108
+ this.ws.send(data);
109
+ if (awaitDrain) {
110
+ await this.waitForDrain(highWaterMark);
111
+ }
112
+ }
113
+ async sendStream(stream, options) {
114
+ for await (const chunk of stream) {
115
+ await this.send(chunk, options);
116
+ }
117
+ }
118
+ sendJSON(data) {
119
+ void this.send(JSON.stringify(data));
120
+ }
121
+ close(code = 1000, reason = '') {
122
+ this.isClosed = true;
123
+ this.closedByUser = true;
124
+ this.stopHeartbeat();
125
+ this.clearReconnectTimer();
126
+ if (this.ws) {
127
+ this.ws.close(code, reason);
128
+ this.ws = null;
129
+ }
130
+ }
131
+ ping() {
132
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
133
+ return;
134
+ const anyWs = this.ws;
135
+ if (typeof anyWs.ping === 'function') {
136
+ try {
137
+ anyWs.ping();
138
+ return;
139
+ }
140
+ catch {
141
+ }
142
+ }
143
+ try {
144
+ this.ws.send('__heartbeat__');
145
+ }
146
+ catch {
147
+ }
148
+ }
149
+ get readyState() {
150
+ return this.ws?.readyState ?? WebSocket.CLOSED;
151
+ }
152
+ get isConnected() {
153
+ return this.ws?.readyState === WebSocket.OPEN;
154
+ }
155
+ toReadable() {
156
+ if (!this.ws)
157
+ return null;
158
+ const wsAny = this.ws;
159
+ if (wsAny.readable) {
160
+ return webToNodeStream(wsAny.readable);
161
+ }
162
+ return null;
163
+ }
164
+ async pipeFrom(source, options) {
165
+ await this.sendStream(source, options);
166
+ }
167
+ async pipeTo(destination) {
168
+ const readable = this.toReadable();
169
+ if (!readable) {
170
+ throw new StreamError('WebSocket has no readable stream', {
171
+ streamType: 'websocket',
172
+ retriable: false,
173
+ });
174
+ }
175
+ await pipeline(readable, destination);
176
+ }
177
+ async *[Symbol.asyncIterator]() {
178
+ const queue = [];
179
+ let resolveNext = null;
180
+ let closed = false;
181
+ const messageHandler = (msg) => {
182
+ if (resolveNext) {
183
+ resolveNext(msg);
184
+ resolveNext = null;
185
+ }
186
+ else {
187
+ queue.push(msg);
188
+ }
189
+ };
190
+ const closeHandler = () => {
191
+ closed = true;
192
+ if (resolveNext) {
193
+ resolveNext(null);
194
+ resolveNext = null;
195
+ }
196
+ };
197
+ this.on('message', messageHandler);
198
+ this.on('close', closeHandler);
199
+ try {
200
+ while (true) {
201
+ if (queue.length > 0) {
202
+ yield queue.shift();
203
+ }
204
+ else {
205
+ if (closed)
206
+ break;
207
+ const msg = await new Promise((resolve) => {
208
+ resolveNext = resolve;
209
+ });
210
+ if (msg) {
211
+ yield msg;
212
+ }
213
+ else {
214
+ break;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ finally {
220
+ this.off('message', messageHandler);
221
+ this.off('close', closeHandler);
222
+ }
223
+ }
224
+ attemptReconnect() {
225
+ if (this.isReconnecting)
226
+ return;
227
+ if (this.options.maxReconnectAttempts > 0 &&
228
+ this.reconnectAttempts >= this.options.maxReconnectAttempts) {
229
+ this.emit('max-reconnect-attempts');
230
+ return;
231
+ }
232
+ this.isReconnecting = true;
233
+ this.reconnectAttempts++;
234
+ const baseDelay = this.backoff.base * Math.pow(this.backoff.factor, this.reconnectAttempts - 1);
235
+ const capped = this.backoff.max ? Math.min(baseDelay, this.backoff.max) : baseDelay;
236
+ const jittered = this.backoff.jitter ? randomJitter(capped) : capped;
237
+ this.emit('reconnecting', this.reconnectAttempts, jittered);
238
+ this.reconnectTimer = setTimeout(() => {
239
+ this.connect().catch((error) => {
240
+ this.emit('reconnect-error', error);
241
+ });
242
+ }, jittered);
243
+ }
244
+ clearReconnectTimer() {
245
+ if (this.reconnectTimer) {
246
+ clearTimeout(this.reconnectTimer);
247
+ this.reconnectTimer = undefined;
248
+ }
249
+ }
250
+ startHeartbeat() {
251
+ if (this.options.heartbeatInterval <= 0)
252
+ return;
253
+ this.heartbeatTimer = setInterval(() => {
254
+ if (this.isConnected) {
255
+ this.ping();
256
+ this.startPongWatchdog();
257
+ }
258
+ }, this.options.heartbeatInterval);
259
+ }
260
+ stopHeartbeat() {
261
+ if (this.heartbeatTimer) {
262
+ clearInterval(this.heartbeatTimer);
263
+ this.heartbeatTimer = undefined;
264
+ }
265
+ this.stopPongWatchdog();
266
+ }
267
+ startPongWatchdog() {
268
+ this.stopPongWatchdog();
269
+ if (this.options.heartbeatTimeout <= 0)
270
+ return;
271
+ this.pongWatchdog = setTimeout(() => {
272
+ this.emit('heartbeat-timeout');
273
+ if (!this.closedByUser && this.options.reconnect) {
274
+ this.ws?.close(4000, 'heartbeat timeout');
275
+ }
276
+ }, this.options.heartbeatTimeout);
277
+ }
278
+ stopPongWatchdog() {
279
+ if (this.pongWatchdog) {
280
+ clearTimeout(this.pongWatchdog);
281
+ this.pongWatchdog = undefined;
282
+ }
283
+ }
284
+ getBufferedAmount() {
285
+ return this.ws?.bufferedAmount ?? 0;
286
+ }
287
+ async waitForDrain(highWaterMark) {
288
+ const buffered = this.getBufferedAmount();
289
+ if (buffered <= highWaterMark)
290
+ return;
291
+ await new Promise((resolve) => {
292
+ const check = () => {
293
+ if (this.getBufferedAmount() <= highWaterMark || !this.isConnected) {
294
+ resolve();
295
+ }
296
+ else {
297
+ setTimeout(check, 10);
298
+ }
299
+ };
300
+ setTimeout(check, 10);
301
+ });
302
+ }
303
+ }
304
+ export function createWebSocket(url, options) {
305
+ const ws = new ReckerWebSocket(url, options);
306
+ ws.connect().catch(() => {
307
+ });
308
+ return ws;
309
+ }
310
+ function randomJitter(value) {
311
+ const jitter = 0.2 * value;
312
+ return value - jitter + Math.random() * (2 * jitter);
313
+ }
@@ -1 +1,2 @@
1
+ #!/usr/bin/env node
1
2
  export {};
package/dist/cli/index.js CHANGED
@@ -1,7 +1,9 @@
1
+ #!/usr/bin/env node
1
2
  import { program } from 'commander';
2
3
  import { promises as fs } from 'node:fs';
3
4
  import { join } from 'node:path';
4
5
  import colors from '../utils/colors.js';
6
+ import { formatColumns } from '../utils/columns.js';
5
7
  async function readStdin() {
6
8
  if (process.stdin.isTTY) {
7
9
  return null;
@@ -119,7 +121,13 @@ async function main() {
119
121
  }
120
122
  return { method, url, headers, data };
121
123
  }
122
- const PRESET_NAMES = Object.keys(presets).filter(k => k !== 'registry' && !k.startsWith('_'));
124
+ const utilityFunctions = [
125
+ 'registry', 'presetRegistry', 'detectPreset', 'getPreset',
126
+ 'listPresets', 'listAIPresets', 'listCloudPresets', 'listSaaSPresets', 'listDevToolsPresets'
127
+ ];
128
+ const PRESET_NAMES = Object.keys(presets)
129
+ .filter(k => !utilityFunctions.includes(k) && !k.startsWith('_') && typeof presets[k] === 'function')
130
+ .sort();
123
131
  program
124
132
  .name('rek')
125
133
  .description('The HTTP Client for Humans (and Robots)')
@@ -130,7 +138,7 @@ async function main() {
130
138
  .option('-o, --output <file>', 'Write response body to file')
131
139
  .option('-j, --json', 'Force JSON content-type')
132
140
  .option('-e, --env [path]', 'Load .env file from current directory or specified path')
133
- .addHelpText('after', `
141
+ .addHelpText('after', () => `
134
142
  ${colors.bold(colors.yellow('Examples:'))}
135
143
  ${colors.green('$ rek httpbin.org/json')}
136
144
  ${colors.green('$ rek post api.com/users name="Cyber" role="Admin"')}
@@ -138,7 +146,7 @@ ${colors.bold(colors.yellow('Examples:'))}
138
146
  ${colors.green('$ rek @openai/v1/chat/completions model="gpt-5.1"')}
139
147
 
140
148
  ${colors.bold(colors.yellow('Available Presets:'))}
141
- ${colors.cyan(PRESET_NAMES.map(p => '@' + p).join(', '))}
149
+ ${formatColumns(PRESET_NAMES, { prefix: '@', indent: 2, minWidth: 16, transform: colors.cyan })}
142
150
  `)
143
151
  .action(async (args, options) => {
144
152
  if (args.length === 0) {
@@ -380,6 +388,94 @@ ${colors.bold('Details:')}`);
380
388
  process.exit(1);
381
389
  }
382
390
  });
391
+ program
392
+ .command('seo')
393
+ .description('Analyze page SEO (title, meta, headings, links, images, structured data)')
394
+ .argument('<url>', 'URL to analyze')
395
+ .option('-a, --all', 'Show all checks including passed ones')
396
+ .addHelpText('after', `
397
+ ${colors.bold(colors.yellow('Examples:'))}
398
+ ${colors.green('$ rek seo example.com')} ${colors.gray('Basic SEO analysis')}
399
+ ${colors.green('$ rek seo example.com -a')} ${colors.gray('Show all checks')}
400
+
401
+ ${colors.bold(colors.yellow('Checks:'))}
402
+ ${colors.cyan('Title Tag')} Length and presence
403
+ ${colors.cyan('Meta Description')} Length and presence
404
+ ${colors.cyan('Headings')} H1 presence and hierarchy
405
+ ${colors.cyan('Images')} Alt text coverage
406
+ ${colors.cyan('Links')} Internal/external distribution
407
+ ${colors.cyan('OpenGraph')} Social sharing meta tags
408
+ ${colors.cyan('Twitter Card')} Twitter sharing meta tags
409
+ ${colors.cyan('Structured Data')} JSON-LD presence
410
+ ${colors.cyan('Technical')} Canonical, viewport, lang
411
+ `)
412
+ .action(async (url, options) => {
413
+ if (!url.startsWith('http'))
414
+ url = `https://${url}`;
415
+ const { createClient } = await import('../core/client.js');
416
+ const { analyzeSeo } = await import('../seo/analyzer.js');
417
+ console.log(colors.gray(`Analyzing SEO for ${url}...`));
418
+ try {
419
+ const client = createClient({ timeout: 30000 });
420
+ const res = await client.get(url);
421
+ const html = await res.text();
422
+ const report = await analyzeSeo(html, { baseUrl: url });
423
+ let gradeColor = colors.red;
424
+ if (report.grade === 'A')
425
+ gradeColor = colors.green;
426
+ else if (report.grade === 'B')
427
+ gradeColor = colors.blue;
428
+ else if (report.grade === 'C')
429
+ gradeColor = colors.yellow;
430
+ console.log(`
431
+ ${colors.bold(colors.cyan('🔍 SEO Analysis Report'))}
432
+ ${colors.gray('URL:')} ${url}
433
+ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray('Score:')} ${report.score}/100
434
+ `);
435
+ if (report.title) {
436
+ console.log(`${colors.bold('Title:')} ${colors.gray(report.title.text.slice(0, 60))}${report.title.text.length > 60 ? '...' : ''} ${colors.gray(`(${report.title.length} chars)`)}`);
437
+ }
438
+ if (report.metaDescription) {
439
+ console.log(`${colors.bold('Description:')} ${colors.gray(report.metaDescription.text.slice(0, 80))}${report.metaDescription.text.length > 80 ? '...' : ''}`);
440
+ }
441
+ console.log('');
442
+ console.log(`${colors.bold('Content Metrics:')}`);
443
+ console.log(` ${colors.gray('Words:')} ${report.content.wordCount} ${colors.gray('Reading time:')} ~${report.content.readingTimeMinutes} min`);
444
+ console.log(` ${colors.gray('Headings:')} H1×${report.headings.h1Count}, total ${report.headings.structure.length}`);
445
+ console.log(` ${colors.gray('Links:')} ${report.links.total} (${report.links.internal} internal, ${report.links.external} external)`);
446
+ console.log(` ${colors.gray('Images:')} ${report.images.total} (${report.images.withAlt} with alt, ${report.images.withoutAlt} without)`);
447
+ console.log('');
448
+ console.log(`${colors.bold('Checks:')}`);
449
+ const checksToShow = options.all
450
+ ? report.checks
451
+ : report.checks.filter(c => c.status !== 'pass' && c.status !== 'info');
452
+ if (checksToShow.length === 0 && !options.all) {
453
+ console.log(colors.green(' All checks passed! Use -a to see details.'));
454
+ }
455
+ else {
456
+ for (const check of checksToShow) {
457
+ const icon = check.status === 'pass' ? colors.green('✔')
458
+ : check.status === 'warn' ? colors.yellow('⚠')
459
+ : check.status === 'fail' ? colors.red('✖')
460
+ : colors.gray('ℹ');
461
+ const name = colors.bold(check.name.padEnd(18));
462
+ console.log(` ${icon} ${name} ${check.message}`);
463
+ if (check.recommendation && check.status !== 'pass') {
464
+ console.log(` ${colors.gray('→')} ${colors.gray(check.recommendation)}`);
465
+ }
466
+ }
467
+ }
468
+ console.log('');
469
+ if (report.jsonLd.count > 0) {
470
+ console.log(`${colors.bold('Structured Data:')} ${report.jsonLd.types.join(', ') || 'Present'}`);
471
+ console.log('');
472
+ }
473
+ }
474
+ catch (error) {
475
+ console.error(colors.red(`SEO analysis failed: ${error.message}`));
476
+ process.exit(1);
477
+ }
478
+ });
383
479
  program
384
480
  .command('ip')
385
481
  .description('Get IP address intelligence using local GeoLite2 database')
package/dist/index.d.ts CHANGED
@@ -42,6 +42,7 @@ export * from './plugins/graphql.js';
42
42
  export * from './plugins/xml.js';
43
43
  export * from './plugins/scrape.js';
44
44
  export * from './scrape/index.js';
45
+ export * from './seo/index.js';
45
46
  export * from './plugins/server-timing.js';
46
47
  export * from './plugins/auth.js';
47
48
  export * from './plugins/proxy-rotator.js';
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ export * from './plugins/graphql.js';
42
42
  export * from './plugins/xml.js';
43
43
  export * from './plugins/scrape.js';
44
44
  export * from './scrape/index.js';
45
+ export * from './seo/index.js';
45
46
  export * from './plugins/server-timing.js';
46
47
  export * from './plugins/auth.js';
47
48
  export * from './plugins/proxy-rotator.js';
@@ -0,0 +1,20 @@
1
+ import type { CheerioAPI } from 'cheerio';
2
+ import type { SeoReport, SeoAnalyzerOptions } from './types.js';
3
+ export declare class SeoAnalyzer {
4
+ private $;
5
+ private options;
6
+ constructor($: CheerioAPI, options?: SeoAnalyzerOptions);
7
+ static fromHtml(html: string, options?: SeoAnalyzerOptions): Promise<SeoAnalyzer>;
8
+ analyze(): SeoReport;
9
+ private analyzeTitle;
10
+ private analyzeDescription;
11
+ private analyzeHeadings;
12
+ private analyzeContent;
13
+ private analyzeLinks;
14
+ private analyzeImages;
15
+ private analyzeSocialMeta;
16
+ private analyzeTechnical;
17
+ private analyzeJsonLd;
18
+ private calculateScore;
19
+ }
20
+ export declare function analyzeSeo(html: string, options?: SeoAnalyzerOptions): Promise<SeoReport>;