recker 1.0.29-next.3524ab6 → 1.0.29-next.cf0cafb

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,35 @@
1
+ import type { ChatMessage } from '../types/ai.js';
2
+ import type { AIMemoryConfig } from '../types/ai-client.js';
3
+ export declare class ConversationMemory {
4
+ private config;
5
+ private systemMessage;
6
+ private messages;
7
+ constructor(config?: AIMemoryConfig);
8
+ setSystemPrompt(prompt: string): void;
9
+ getSystemPrompt(): string;
10
+ addUserMessage(content: string): void;
11
+ addAssistantMessage(content: string): void;
12
+ addMessage(message: ChatMessage): void;
13
+ buildMessages(userPrompt: string): ChatMessage[];
14
+ recordResponse(content: string): void;
15
+ getMessages(): ChatMessage[];
16
+ getConversation(): readonly ChatMessage[];
17
+ getPairCount(): number;
18
+ clear(): void;
19
+ reset(): void;
20
+ setConfig(config: Partial<AIMemoryConfig>): void;
21
+ getConfig(): AIMemoryConfig;
22
+ private prune;
23
+ isEmpty(): boolean;
24
+ getMessageCount(): number;
25
+ toJSON(): {
26
+ config: AIMemoryConfig;
27
+ systemPrompt: string | null;
28
+ messages: ChatMessage[];
29
+ };
30
+ static fromJSON(data: {
31
+ config?: AIMemoryConfig;
32
+ systemPrompt?: string | null;
33
+ messages?: ChatMessage[];
34
+ }): ConversationMemory;
35
+ }
@@ -0,0 +1,136 @@
1
+ const DEFAULT_MAX_PAIRS = 12;
2
+ export class ConversationMemory {
3
+ config;
4
+ systemMessage = null;
5
+ messages = [];
6
+ constructor(config = {}) {
7
+ this.config = {
8
+ maxPairs: config.maxPairs ?? DEFAULT_MAX_PAIRS,
9
+ systemPrompt: config.systemPrompt ?? '',
10
+ };
11
+ if (this.config.systemPrompt) {
12
+ this.systemMessage = {
13
+ role: 'system',
14
+ content: this.config.systemPrompt,
15
+ };
16
+ }
17
+ }
18
+ setSystemPrompt(prompt) {
19
+ this.config.systemPrompt = prompt;
20
+ if (prompt) {
21
+ this.systemMessage = {
22
+ role: 'system',
23
+ content: prompt,
24
+ };
25
+ }
26
+ else {
27
+ this.systemMessage = null;
28
+ }
29
+ }
30
+ getSystemPrompt() {
31
+ return this.config.systemPrompt;
32
+ }
33
+ addUserMessage(content) {
34
+ this.messages.push({
35
+ role: 'user',
36
+ content,
37
+ });
38
+ this.prune();
39
+ }
40
+ addAssistantMessage(content) {
41
+ this.messages.push({
42
+ role: 'assistant',
43
+ content,
44
+ });
45
+ this.prune();
46
+ }
47
+ addMessage(message) {
48
+ if (message.role === 'system') {
49
+ this.setSystemPrompt(typeof message.content === 'string' ? message.content : '');
50
+ return;
51
+ }
52
+ this.messages.push(message);
53
+ this.prune();
54
+ }
55
+ buildMessages(userPrompt) {
56
+ this.addUserMessage(userPrompt);
57
+ return this.getMessages();
58
+ }
59
+ recordResponse(content) {
60
+ this.addAssistantMessage(content);
61
+ }
62
+ getMessages() {
63
+ const result = [];
64
+ if (this.systemMessage) {
65
+ result.push(this.systemMessage);
66
+ }
67
+ result.push(...this.messages);
68
+ return result;
69
+ }
70
+ getConversation() {
71
+ return this.messages;
72
+ }
73
+ getPairCount() {
74
+ let pairs = 0;
75
+ for (let i = 0; i < this.messages.length - 1; i += 2) {
76
+ if (this.messages[i].role === 'user' &&
77
+ this.messages[i + 1]?.role === 'assistant') {
78
+ pairs++;
79
+ }
80
+ }
81
+ return pairs;
82
+ }
83
+ clear() {
84
+ this.messages = [];
85
+ }
86
+ reset() {
87
+ this.messages = [];
88
+ this.systemMessage = null;
89
+ this.config.systemPrompt = '';
90
+ }
91
+ setConfig(config) {
92
+ if (config.maxPairs !== undefined) {
93
+ this.config.maxPairs = config.maxPairs;
94
+ this.prune();
95
+ }
96
+ if (config.systemPrompt !== undefined) {
97
+ this.setSystemPrompt(config.systemPrompt);
98
+ }
99
+ }
100
+ getConfig() {
101
+ return { ...this.config };
102
+ }
103
+ prune() {
104
+ const maxMessages = this.config.maxPairs * 2;
105
+ if (this.messages.length > maxMessages) {
106
+ const excess = this.messages.length - maxMessages;
107
+ const toRemove = Math.ceil(excess / 2) * 2;
108
+ this.messages = this.messages.slice(toRemove);
109
+ }
110
+ }
111
+ isEmpty() {
112
+ return this.messages.length === 0;
113
+ }
114
+ getMessageCount() {
115
+ return this.messages.length;
116
+ }
117
+ toJSON() {
118
+ return {
119
+ config: this.config,
120
+ systemPrompt: this.systemMessage?.content,
121
+ messages: [...this.messages],
122
+ };
123
+ }
124
+ static fromJSON(data) {
125
+ const memory = new ConversationMemory(data.config);
126
+ if (data.systemPrompt) {
127
+ memory.setSystemPrompt(data.systemPrompt);
128
+ }
129
+ if (data.messages) {
130
+ for (const msg of data.messages) {
131
+ memory.addMessage(msg);
132
+ }
133
+ }
134
+ return memory;
135
+ }
136
+ }
@@ -0,0 +1,29 @@
1
+ import type { AIResponse, AIStream, ChatMessage, AIProvider } from './ai.js';
2
+ export interface AIMemoryConfig {
3
+ maxPairs?: number;
4
+ systemPrompt?: string;
5
+ }
6
+ export interface PresetAIConfig {
7
+ provider: AIProvider;
8
+ apiKey?: string;
9
+ model: string;
10
+ baseUrl?: string;
11
+ memory?: AIMemoryConfig;
12
+ organization?: string;
13
+ headers?: Record<string, string>;
14
+ }
15
+ export interface ClientAI {
16
+ chat(prompt: string): Promise<AIResponse>;
17
+ chatStream(prompt: string): Promise<AIStream>;
18
+ prompt(prompt: string): Promise<AIResponse>;
19
+ promptStream(prompt: string): Promise<AIStream>;
20
+ clearMemory(): void;
21
+ getMemory(): readonly ChatMessage[];
22
+ setMemoryConfig(config: Partial<AIMemoryConfig>): void;
23
+ getMemoryConfig(): AIMemoryConfig;
24
+ readonly provider: AIProvider;
25
+ readonly model: string;
26
+ }
27
+ export interface ClientOptionsWithAI {
28
+ _aiConfig?: PresetAIConfig;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1517,7 +1517,7 @@ ${colors.bold('Network:')}
1517
1517
  }
1518
1518
  async runSpider(args) {
1519
1519
  let url = '';
1520
- let maxDepth = 3;
1520
+ let maxDepth = 5;
1521
1521
  let maxPages = 100;
1522
1522
  let concurrency = 5;
1523
1523
  let seoEnabled = false;
@@ -1525,7 +1525,7 @@ ${colors.bold('Network:')}
1525
1525
  for (let i = 0; i < args.length; i++) {
1526
1526
  const arg = args[i];
1527
1527
  if (arg.startsWith('depth=')) {
1528
- maxDepth = parseInt(arg.split('=')[1]) || 4;
1528
+ maxDepth = parseInt(arg.split('=')[1]) || 5;
1529
1529
  }
1530
1530
  else if (arg.startsWith('limit=')) {
1531
1531
  maxPages = parseInt(arg.split('=')[1]) || 100;
@@ -1547,7 +1547,7 @@ ${colors.bold('Network:')}
1547
1547
  if (!this.baseUrl) {
1548
1548
  console.log(colors.yellow('Usage: spider <url> [options]'));
1549
1549
  console.log(colors.gray(' Options:'));
1550
- console.log(colors.gray(' depth=4 Max crawl depth'));
1550
+ console.log(colors.gray(' depth=5 Max crawl depth'));
1551
1551
  console.log(colors.gray(' limit=100 Max pages to crawl'));
1552
1552
  console.log(colors.gray(' concurrency=5 Concurrent requests'));
1553
1553
  console.log(colors.gray(' seo Enable SEO analysis'));
@@ -2710,7 +2710,7 @@ ${colors.bold('Network:')}
2710
2710
  ${colors.bold('Web Crawler:')}
2711
2711
  ${colors.green('spider <url>')} Crawl website following internal links.
2712
2712
  ${colors.gray('Options:')}
2713
- ${colors.white('--depth=4')} ${colors.gray('Maximum depth to crawl')}
2713
+ ${colors.white('--depth=5')} ${colors.gray('Maximum depth to crawl')}
2714
2714
  ${colors.white('--limit=100')} ${colors.gray('Maximum pages to crawl')}
2715
2715
  ${colors.white('--concurrency=5')} ${colors.gray('Parallel requests')}
2716
2716
 
@@ -9,6 +9,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
11
  import { seoTools, seoToolHandlers } from './tools/seo.js';
12
+ import { scrapeTools, scrapeToolHandlers } from './tools/scrape.js';
13
+ import { securityTools, securityToolHandlers } from './tools/security.js';
12
14
  import { ToolRegistry } from './tools/registry.js';
13
15
  import { loadToolModules } from './tools/loader.js';
14
16
  export class MCPServer {
@@ -50,6 +52,14 @@ export class MCPServer {
50
52
  tools: seoTools,
51
53
  handlers: seoToolHandlers
52
54
  });
55
+ this.toolRegistry.registerModule({
56
+ tools: scrapeTools,
57
+ handlers: scrapeToolHandlers
58
+ });
59
+ this.toolRegistry.registerModule({
60
+ tools: securityTools,
61
+ handlers: securityToolHandlers
62
+ });
53
63
  }
54
64
  indexReady = null;
55
65
  async ensureIndexReady() {
@@ -0,0 +1,3 @@
1
+ import type { MCPTool, MCPToolResult } from '../types.js';
2
+ export declare const scrapeTools: MCPTool[];
3
+ export declare const scrapeToolHandlers: Record<string, (args: Record<string, unknown>) => Promise<MCPToolResult>>;
@@ -0,0 +1,156 @@
1
+ import { createClient } from '../../core/client.js';
2
+ import { ScrapeDocument } from '../../scrape/document.js';
3
+ async function scrapeUrl(args) {
4
+ const url = String(args.url || '');
5
+ const selectors = args.selectors;
6
+ const extract = args.extract;
7
+ const selector = args.selector;
8
+ if (!url) {
9
+ return {
10
+ content: [{ type: 'text', text: 'Error: url is required' }],
11
+ isError: true,
12
+ };
13
+ }
14
+ try {
15
+ const client = createClient({ timeout: 30000 });
16
+ const response = await client.get(url);
17
+ const html = await response.text();
18
+ const doc = await ScrapeDocument.create(html, { baseUrl: url });
19
+ const output = {
20
+ url,
21
+ title: doc.title(),
22
+ };
23
+ if (selector) {
24
+ const elements = doc.selectAll(selector);
25
+ output.results = elements.map(el => ({
26
+ text: el.text(),
27
+ html: el.html(),
28
+ tag: el.tagName(),
29
+ attrs: el.attrs(),
30
+ }));
31
+ output.count = elements.length;
32
+ }
33
+ if (selectors && Object.keys(selectors).length > 0) {
34
+ const extracted = {};
35
+ for (const [key, sel] of Object.entries(selectors)) {
36
+ const isMultiple = sel.endsWith('[]');
37
+ const actualSel = isMultiple ? sel.slice(0, -2) : sel;
38
+ if (isMultiple) {
39
+ extracted[key] = doc.texts(actualSel);
40
+ }
41
+ else {
42
+ extracted[key] = doc.text(actualSel);
43
+ }
44
+ }
45
+ output.data = extracted;
46
+ }
47
+ const extractSet = new Set(extract || []);
48
+ if (extractSet.has('links') || extractSet.has('all')) {
49
+ const links = doc.links({ absolute: true });
50
+ output.links = links.slice(0, 100).map(l => ({
51
+ href: l.href,
52
+ text: l.text?.slice(0, 100),
53
+ rel: l.rel,
54
+ }));
55
+ output.linkCount = links.length;
56
+ }
57
+ if (extractSet.has('images') || extractSet.has('all')) {
58
+ const images = doc.images({ absolute: true });
59
+ output.images = images.slice(0, 50).map(img => ({
60
+ src: img.src,
61
+ alt: img.alt,
62
+ width: img.width,
63
+ height: img.height,
64
+ }));
65
+ output.imageCount = images.length;
66
+ }
67
+ if (extractSet.has('meta') || extractSet.has('all')) {
68
+ output.meta = doc.meta();
69
+ }
70
+ if (extractSet.has('og') || extractSet.has('all')) {
71
+ output.openGraph = doc.openGraph();
72
+ }
73
+ if (extractSet.has('twitter') || extractSet.has('all')) {
74
+ output.twitterCard = doc.twitterCard();
75
+ }
76
+ if (extractSet.has('jsonld') || extractSet.has('all')) {
77
+ output.jsonLd = doc.jsonLd();
78
+ }
79
+ if (extractSet.has('tables') || extractSet.has('all')) {
80
+ const tables = doc.tables();
81
+ output.tables = tables.slice(0, 10).map(t => ({
82
+ headers: t.headers,
83
+ rows: t.rows.slice(0, 50),
84
+ }));
85
+ output.tableCount = tables.length;
86
+ }
87
+ if (extractSet.has('forms') || extractSet.has('all')) {
88
+ output.forms = doc.forms();
89
+ }
90
+ if (extractSet.has('headings')) {
91
+ output.headings = {
92
+ h1: doc.texts('h1'),
93
+ h2: doc.texts('h2'),
94
+ h3: doc.texts('h3'),
95
+ };
96
+ }
97
+ return {
98
+ content: [{
99
+ type: 'text',
100
+ text: JSON.stringify(output, null, 2),
101
+ }],
102
+ };
103
+ }
104
+ catch (error) {
105
+ return {
106
+ content: [{
107
+ type: 'text',
108
+ text: `Scrape failed: ${error.message}`,
109
+ }],
110
+ isError: true,
111
+ };
112
+ }
113
+ }
114
+ export const scrapeTools = [
115
+ {
116
+ name: 'rek_scrape',
117
+ description: `Scrape a web page and extract data using CSS selectors.
118
+
119
+ Supports multiple extraction modes:
120
+ - Single selector: Extract elements matching one CSS selector
121
+ - Selector map: Extract multiple fields at once
122
+ - Built-in extractors: links, images, meta, og, twitter, jsonld, tables, forms, headings
123
+
124
+ Examples:
125
+ - Get all product titles: selector=".product-title"
126
+ - Extract multiple fields: selectors={"title":"h1","price":".price","desc":".description"}
127
+ - Get all links and images: extract=["links","images"]
128
+ - Full extraction: extract=["all"]`,
129
+ inputSchema: {
130
+ type: 'object',
131
+ properties: {
132
+ url: {
133
+ type: 'string',
134
+ description: 'URL to scrape',
135
+ },
136
+ selector: {
137
+ type: 'string',
138
+ description: 'Single CSS selector to extract elements (e.g., ".product-card", "article h2")',
139
+ },
140
+ selectors: {
141
+ type: 'object',
142
+ description: 'Map of field names to CSS selectors. Add [] suffix for multiple values (e.g., {"title":"h1","links[]":"a"})',
143
+ },
144
+ extract: {
145
+ type: 'array',
146
+ items: { type: 'string' },
147
+ description: 'Built-in extractors to run: links, images, meta, og, twitter, jsonld, tables, forms, headings, all',
148
+ },
149
+ },
150
+ required: ['url'],
151
+ },
152
+ },
153
+ ];
154
+ export const scrapeToolHandlers = {
155
+ rek_scrape: scrapeUrl,
156
+ };
@@ -0,0 +1,3 @@
1
+ import type { MCPTool, MCPToolResult } from '../types.js';
2
+ export declare const securityTools: MCPTool[];
3
+ export declare const securityToolHandlers: Record<string, (args: Record<string, unknown>) => Promise<MCPToolResult>>;
@@ -0,0 +1,471 @@
1
+ import { createClient } from '../../core/client.js';
2
+ import { inspectTLS } from '../../utils/tls-inspector.js';
3
+ import { rdap, supportsRDAP } from '../../utils/rdap.js';
4
+ import { getIpInfo, isValidIP } from '../ip-intel.js';
5
+ import { analyzeSecurityHeaders, quickSecurityCheck } from '../../utils/security-grader.js';
6
+ import { validateSpf, validateDmarc, checkDkim, checkDnsHealth, getSecurityRecords, } from '../../utils/dns-toolkit.js';
7
+ async function tlsInspect(args) {
8
+ const host = String(args.host || '');
9
+ const port = Number(args.port) || 443;
10
+ if (!host) {
11
+ return {
12
+ content: [{ type: 'text', text: 'Error: host is required' }],
13
+ isError: true,
14
+ };
15
+ }
16
+ try {
17
+ const info = await inspectTLS(host, port);
18
+ const output = {
19
+ host,
20
+ port,
21
+ valid: info.valid,
22
+ authorized: info.authorized,
23
+ certificate: {
24
+ subject: info.subject,
25
+ issuer: info.issuer,
26
+ validFrom: info.validFrom.toISOString(),
27
+ validTo: info.validTo.toISOString(),
28
+ daysRemaining: info.daysRemaining,
29
+ serialNumber: info.serialNumber,
30
+ fingerprint256: info.fingerprint256,
31
+ },
32
+ connection: {
33
+ protocol: info.protocol,
34
+ cipher: info.cipher,
35
+ },
36
+ subjectAltNames: info.altNames,
37
+ publicKey: info.pubkey,
38
+ extendedKeyUsage: info.extKeyUsage,
39
+ warnings: [],
40
+ };
41
+ if (info.daysRemaining <= 30) {
42
+ output.warnings.push(`Certificate expires in ${info.daysRemaining} days!`);
43
+ }
44
+ if (!info.authorized) {
45
+ output.warnings.push(`Certificate not trusted: ${info.authorizationError?.message || 'unknown reason'}`);
46
+ }
47
+ if (info.pubkey && info.pubkey.algo === 'rsa' && info.pubkey.size < 2048) {
48
+ output.warnings.push(`RSA key size ${info.pubkey.size} bits is considered weak`);
49
+ }
50
+ return {
51
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
52
+ };
53
+ }
54
+ catch (error) {
55
+ return {
56
+ content: [{ type: 'text', text: `TLS inspection failed: ${error.message}` }],
57
+ isError: true,
58
+ };
59
+ }
60
+ }
61
+ async function rdapLookup(args) {
62
+ const query = String(args.query || '');
63
+ if (!query) {
64
+ return {
65
+ content: [{ type: 'text', text: 'Error: query (domain or IP) is required' }],
66
+ isError: true,
67
+ };
68
+ }
69
+ if (!query.includes(':') && !/^\d+\.\d+\.\d+\.\d+$/.test(query)) {
70
+ const tld = query.split('.').pop()?.toLowerCase() || '';
71
+ if (!supportsRDAP(tld)) {
72
+ return {
73
+ content: [{
74
+ type: 'text',
75
+ text: `RDAP is not available for .${tld} domains. Use rek_whois_lookup instead for traditional WHOIS data.`,
76
+ }],
77
+ isError: true,
78
+ };
79
+ }
80
+ }
81
+ try {
82
+ const client = createClient({ timeout: 15000 });
83
+ const result = await rdap(client, query);
84
+ const output = {
85
+ query,
86
+ ldhName: result.ldhName,
87
+ handle: result.handle,
88
+ status: result.status,
89
+ };
90
+ if (result.events) {
91
+ output.events = {};
92
+ for (const event of result.events) {
93
+ output.events[event.eventAction] = event.eventDate;
94
+ }
95
+ }
96
+ if (result.nameservers) {
97
+ output.nameservers = result.nameservers.map(ns => ns.ldhName);
98
+ }
99
+ if (result.entities) {
100
+ output.entities = result.entities.map(entity => ({
101
+ handle: entity.handle,
102
+ roles: entity.roles,
103
+ }));
104
+ }
105
+ if (result.secureDNS) {
106
+ output.dnssec = result.secureDNS.delegationSigned ? 'signed' : 'unsigned';
107
+ }
108
+ return {
109
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
110
+ };
111
+ }
112
+ catch (error) {
113
+ return {
114
+ content: [{ type: 'text', text: `RDAP lookup failed: ${error.message}` }],
115
+ isError: true,
116
+ };
117
+ }
118
+ }
119
+ async function geoipLookup(args) {
120
+ const ip = String(args.ip || '');
121
+ if (!ip) {
122
+ return {
123
+ content: [{ type: 'text', text: 'Error: ip address is required' }],
124
+ isError: true,
125
+ };
126
+ }
127
+ if (!isValidIP(ip)) {
128
+ return {
129
+ content: [{ type: 'text', text: `Invalid IP address: ${ip}` }],
130
+ isError: true,
131
+ };
132
+ }
133
+ try {
134
+ const info = await getIpInfo(ip);
135
+ const output = {
136
+ ip: info.ip,
137
+ isIPv6: info.isIPv6,
138
+ };
139
+ if (info.bogon) {
140
+ output.bogon = true;
141
+ output.bogonType = info.bogonType;
142
+ output.note = 'This is a private/reserved IP address. No geolocation available.';
143
+ }
144
+ else {
145
+ output.location = {
146
+ city: info.city,
147
+ region: info.region,
148
+ country: info.country,
149
+ countryCode: info.countryCode,
150
+ continent: info.continent,
151
+ coordinates: info.loc,
152
+ timezone: info.timezone,
153
+ postalCode: info.postal,
154
+ accuracyRadius: info.accuracy ? `${info.accuracy} km` : undefined,
155
+ };
156
+ }
157
+ return {
158
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
159
+ };
160
+ }
161
+ catch (error) {
162
+ return {
163
+ content: [{ type: 'text', text: `GeoIP lookup failed: ${error.message}` }],
164
+ isError: true,
165
+ };
166
+ }
167
+ }
168
+ async function securityHeadersAnalyze(args) {
169
+ const url = String(args.url || '');
170
+ const quick = Boolean(args.quick);
171
+ if (!url) {
172
+ return {
173
+ content: [{ type: 'text', text: 'Error: url is required' }],
174
+ isError: true,
175
+ };
176
+ }
177
+ try {
178
+ const client = createClient({ timeout: 15000 });
179
+ const response = await client.head(url);
180
+ if (quick) {
181
+ const result = quickSecurityCheck(response.headers);
182
+ return {
183
+ content: [{
184
+ type: 'text',
185
+ text: JSON.stringify({
186
+ url,
187
+ secure: result.secure,
188
+ criticalIssues: result.criticalIssues,
189
+ }, null, 2),
190
+ }],
191
+ };
192
+ }
193
+ const report = analyzeSecurityHeaders(response.headers);
194
+ const output = {
195
+ url,
196
+ grade: report.grade,
197
+ score: report.score,
198
+ summary: report.summary,
199
+ };
200
+ const failed = report.details.filter(d => d.status === 'fail');
201
+ const warnings = report.details.filter(d => d.status === 'warn');
202
+ const passed = report.details.filter(d => d.status === 'pass');
203
+ if (failed.length > 0) {
204
+ output.failed = failed.map(d => ({
205
+ header: d.header,
206
+ message: d.message,
207
+ recommendation: d.recommendation,
208
+ }));
209
+ }
210
+ if (warnings.length > 0) {
211
+ output.warnings = warnings.map(d => ({
212
+ header: d.header,
213
+ message: d.message,
214
+ recommendation: d.recommendation,
215
+ }));
216
+ }
217
+ output.passed = passed.map(d => d.header);
218
+ if (report.csp) {
219
+ output.csp = {
220
+ score: report.csp.score,
221
+ issues: report.csp.issues,
222
+ missingDirectives: report.csp.missingDirectives,
223
+ hasUnsafeInline: report.csp.hasUnsafeInline,
224
+ hasUnsafeEval: report.csp.hasUnsafeEval,
225
+ };
226
+ }
227
+ return {
228
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
229
+ };
230
+ }
231
+ catch (error) {
232
+ return {
233
+ content: [{ type: 'text', text: `Security headers analysis failed: ${error.message}` }],
234
+ isError: true,
235
+ };
236
+ }
237
+ }
238
+ async function dnsToolkit(args) {
239
+ const domain = String(args.domain || '');
240
+ const check = String(args.check || 'all');
241
+ const dkimSelector = args.dkimSelector;
242
+ if (!domain) {
243
+ return {
244
+ content: [{ type: 'text', text: 'Error: domain is required' }],
245
+ isError: true,
246
+ };
247
+ }
248
+ try {
249
+ const output = { domain };
250
+ if (check === 'all' || check === 'health') {
251
+ const healthReport = await checkDnsHealth(domain);
252
+ output.health = {
253
+ score: healthReport.score,
254
+ grade: healthReport.grade,
255
+ checks: healthReport.checks,
256
+ };
257
+ }
258
+ if (check === 'all' || check === 'spf') {
259
+ const spfResult = await validateSpf(domain);
260
+ output.spf = {
261
+ valid: spfResult.valid,
262
+ record: spfResult.record,
263
+ mechanisms: spfResult.mechanisms,
264
+ lookupCount: spfResult.lookupCount,
265
+ warnings: spfResult.warnings,
266
+ errors: spfResult.errors,
267
+ };
268
+ }
269
+ if (check === 'all' || check === 'dmarc') {
270
+ const dmarcResult = await validateDmarc(domain);
271
+ output.dmarc = {
272
+ valid: dmarcResult.valid,
273
+ record: dmarcResult.record,
274
+ policy: dmarcResult.policy,
275
+ subdomainPolicy: dmarcResult.subdomainPolicy,
276
+ percentage: dmarcResult.percentage,
277
+ reportingUris: dmarcResult.rua,
278
+ warnings: dmarcResult.warnings,
279
+ };
280
+ }
281
+ if (check === 'all' || check === 'dkim') {
282
+ const selectors = dkimSelector
283
+ ? [dkimSelector]
284
+ : ['default', 'google', 'selector1', 'selector2', 'k1', 's1', 'dkim'];
285
+ const dkimResults = [];
286
+ for (const sel of selectors) {
287
+ const result = await checkDkim(domain, sel);
288
+ if (result.found) {
289
+ dkimResults.push({
290
+ selector: sel,
291
+ found: true,
292
+ publicKey: result.publicKey?.slice(0, 50) + '...',
293
+ });
294
+ }
295
+ }
296
+ output.dkim = {
297
+ found: dkimResults.length > 0,
298
+ selectors: dkimResults,
299
+ note: dkimResults.length === 0
300
+ ? 'No DKIM records found with common selectors. Specify dkimSelector parameter if you know the selector.'
301
+ : undefined,
302
+ };
303
+ }
304
+ if (check === 'all' || check === 'records') {
305
+ const records = await getSecurityRecords(domain);
306
+ output.securityRecords = {
307
+ spf: records.spf,
308
+ dmarc: records.dmarc,
309
+ caa: records.caa,
310
+ mx: records.mx,
311
+ };
312
+ }
313
+ return {
314
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
315
+ };
316
+ }
317
+ catch (error) {
318
+ return {
319
+ content: [{ type: 'text', text: `DNS toolkit failed: ${error.message}` }],
320
+ isError: true,
321
+ };
322
+ }
323
+ }
324
+ export const securityTools = [
325
+ {
326
+ name: 'rek_tls_inspect',
327
+ description: `Inspect SSL/TLS certificate and connection details for a host.
328
+
329
+ Returns:
330
+ - Certificate validity and expiration
331
+ - Subject and issuer details
332
+ - Subject Alternative Names (SANs)
333
+ - TLS protocol version and cipher suite
334
+ - Public key algorithm and size
335
+ - Warnings for expiring certs, weak keys, or trust issues
336
+
337
+ Use this to check certificate health, debug TLS issues, or audit HTTPS configuration.`,
338
+ inputSchema: {
339
+ type: 'object',
340
+ properties: {
341
+ host: {
342
+ type: 'string',
343
+ description: 'Hostname to inspect (e.g., "example.com")',
344
+ },
345
+ port: {
346
+ type: 'number',
347
+ description: 'Port number (default: 443)',
348
+ default: 443,
349
+ },
350
+ },
351
+ required: ['host'],
352
+ },
353
+ },
354
+ {
355
+ name: 'rek_rdap_lookup',
356
+ description: `Perform RDAP lookup (modern WHOIS) for a domain or IP address.
357
+
358
+ RDAP provides structured JSON data including:
359
+ - Registration dates (created, updated, expires)
360
+ - Status (active, locked, etc.)
361
+ - Nameservers
362
+ - DNSSEC status
363
+ - Registrar/Registrant info
364
+
365
+ Note: Some TLDs (.io, .ai, etc.) don't support RDAP yet - use rek_whois_lookup for those.`,
366
+ inputSchema: {
367
+ type: 'object',
368
+ properties: {
369
+ query: {
370
+ type: 'string',
371
+ description: 'Domain name or IP address to lookup',
372
+ },
373
+ },
374
+ required: ['query'],
375
+ },
376
+ },
377
+ {
378
+ name: 'rek_geoip_lookup',
379
+ description: `Get geolocation data for an IP address using MaxMind GeoLite2 database.
380
+
381
+ Returns:
382
+ - City, region, country, continent
383
+ - Coordinates (latitude, longitude)
384
+ - Timezone
385
+ - Accuracy radius
386
+ - Bogon detection (identifies private/reserved IPs)
387
+
388
+ Database is cached locally and updated automatically.`,
389
+ inputSchema: {
390
+ type: 'object',
391
+ properties: {
392
+ ip: {
393
+ type: 'string',
394
+ description: 'IPv4 or IPv6 address to lookup',
395
+ },
396
+ },
397
+ required: ['ip'],
398
+ },
399
+ },
400
+ {
401
+ name: 'rek_security_headers',
402
+ description: `Analyze HTTP security headers for a URL.
403
+
404
+ Grades (A+ to F) based on:
405
+ - HSTS (Strict-Transport-Security)
406
+ - CSP (Content-Security-Policy) with detailed analysis
407
+ - X-Frame-Options / frame-ancestors
408
+ - X-Content-Type-Options
409
+ - Referrer-Policy
410
+ - Permissions-Policy
411
+ - Cross-Origin policies (COOP, COEP, CORP)
412
+ - Information leakage (Server, X-Powered-By)
413
+
414
+ Use quick=true for a fast critical issues check.`,
415
+ inputSchema: {
416
+ type: 'object',
417
+ properties: {
418
+ url: {
419
+ type: 'string',
420
+ description: 'URL to analyze',
421
+ },
422
+ quick: {
423
+ type: 'boolean',
424
+ description: 'Quick mode - only check critical security issues',
425
+ default: false,
426
+ },
427
+ },
428
+ required: ['url'],
429
+ },
430
+ },
431
+ {
432
+ name: 'rek_dns_toolkit',
433
+ description: `Advanced DNS security analysis for email authentication and DNS health.
434
+
435
+ Checks:
436
+ - SPF validation (syntax, lookup count, mechanisms)
437
+ - DMARC validation (policy, reporting, alignment)
438
+ - DKIM discovery (tries common selectors)
439
+ - CAA records (certificate authority authorization)
440
+ - MX records
441
+ - Overall DNS health score
442
+
443
+ Use check parameter to run specific checks: "all", "health", "spf", "dmarc", "dkim", "records"`,
444
+ inputSchema: {
445
+ type: 'object',
446
+ properties: {
447
+ domain: {
448
+ type: 'string',
449
+ description: 'Domain to analyze',
450
+ },
451
+ check: {
452
+ type: 'string',
453
+ description: 'Which check to run: all, health, spf, dmarc, dkim, records',
454
+ default: 'all',
455
+ },
456
+ dkimSelector: {
457
+ type: 'string',
458
+ description: 'Specific DKIM selector to check (if known)',
459
+ },
460
+ },
461
+ required: ['domain'],
462
+ },
463
+ },
464
+ ];
465
+ export const securityToolHandlers = {
466
+ rek_tls_inspect: tlsInspect,
467
+ rek_rdap_lookup: rdapLookup,
468
+ rek_geoip_lookup: geoipLookup,
469
+ rek_security_headers: securityHeadersAnalyze,
470
+ rek_dns_toolkit: dnsToolkit,
471
+ };
@@ -160,8 +160,8 @@ async function seoAnalyze(args) {
160
160
  }
161
161
  async function seoSpiderCrawl(args) {
162
162
  const url = String(args.url || '');
163
- const maxPages = Number(args.maxPages) || 20;
164
- const maxDepth = Number(args.maxDepth) || 3;
163
+ const maxPages = Number(args.maxPages) || 100;
164
+ const maxDepth = Number(args.maxDepth) || 5;
165
165
  const concurrency = Number(args.concurrency) || 3;
166
166
  if (!url) {
167
167
  return {
@@ -377,12 +377,12 @@ Returns per-page scores and prioritized recommendations for improving overall si
377
377
  maxPages: {
378
378
  type: 'number',
379
379
  description: 'Maximum pages to crawl',
380
- default: 20,
380
+ default: 100,
381
381
  },
382
382
  maxDepth: {
383
383
  type: 'number',
384
384
  description: 'Maximum link depth to follow',
385
- default: 3,
385
+ default: 5,
386
386
  },
387
387
  concurrency: {
388
388
  type: 'number',
@@ -86,7 +86,7 @@ export class Spider {
86
86
  pendingCount = 0;
87
87
  constructor(options = {}) {
88
88
  this.options = {
89
- maxDepth: options.maxDepth ?? 4,
89
+ maxDepth: options.maxDepth ?? 5,
90
90
  maxPages: options.maxPages ?? 100,
91
91
  sameDomain: options.sameDomain ?? true,
92
92
  concurrency: options.concurrency ?? 5,
@@ -0,0 +1,29 @@
1
+ import type { AIResponse, AIStream, ChatMessage, AIProvider } from './ai.js';
2
+ export interface AIMemoryConfig {
3
+ maxPairs?: number;
4
+ systemPrompt?: string;
5
+ }
6
+ export interface PresetAIConfig {
7
+ provider: AIProvider;
8
+ apiKey?: string;
9
+ model: string;
10
+ baseUrl?: string;
11
+ memory?: AIMemoryConfig;
12
+ organization?: string;
13
+ headers?: Record<string, string>;
14
+ }
15
+ export interface ClientAI {
16
+ chat(prompt: string): Promise<AIResponse>;
17
+ chatStream(prompt: string): Promise<AIStream>;
18
+ prompt(prompt: string): Promise<AIResponse>;
19
+ promptStream(prompt: string): Promise<AIStream>;
20
+ clearMemory(): void;
21
+ getMemory(): readonly ChatMessage[];
22
+ setMemoryConfig(config: Partial<AIMemoryConfig>): void;
23
+ getMemoryConfig(): AIMemoryConfig;
24
+ readonly provider: AIProvider;
25
+ readonly model: string;
26
+ }
27
+ export interface ClientOptionsWithAI {
28
+ _aiConfig?: PresetAIConfig;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
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.cf0cafb",
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",