@oevortex/ddg_search 1.2.0 → 1.2.2

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.
@@ -2,241 +2,241 @@ import axios from 'axios';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { getRandomUserAgent } from './user_agents.js';
4
4
 
5
- class MonicaClient {
6
- constructor(timeout = 60000) {
7
- this.apiEndpoint = "https://monica.so/api/search_v1/search";
8
- this.timeout = timeout;
9
- this.clientId = randomUUID();
10
- this.sessionId = "";
11
-
12
- this.headers = {
13
- "accept": "*/*",
14
- "accept-encoding": "gzip, deflate, br, zstd",
15
- "accept-language": "en-US,en;q=0.9",
16
- "content-type": "application/json",
17
- "dnt": "1",
18
- "origin": "https://monica.so",
19
- "referer": "https://monica.so/answers",
20
- "sec-ch-ua": '"Microsoft Edge";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
21
- "sec-ch-ua-mobile": "?0",
22
- "sec-ch-ua-platform": '"Windows"',
23
- "sec-fetch-dest": "empty",
24
- "sec-fetch-mode": "cors",
25
- "sec-fetch-site": "same-origin",
26
- "sec-gpc": "1",
27
- "user-agent": getRandomUserAgent(),
28
- "x-client-id": this.clientId,
29
- "x-client-locale": "en",
30
- "x-client-type": "web",
31
- "x-client-version": "5.4.3",
32
- "x-from-channel": "NA",
33
- "x-product-name": "Monica-Search",
34
- "x-time-zone": "Asia/Calcutta;-330"
35
- };
36
-
37
- // Axios instance with improved configuration
38
- this.client = axios.create({
39
- headers: this.headers,
40
- timeout: this.timeout,
41
- withCredentials: true,
42
- validateStatus: (status) => status >= 200 && status < 500 // Accept non-error status codes
43
- });
44
- }
45
-
46
- formatResponse(text) {
47
- try {
48
- // Clean up markdown formatting
49
- let cleanedText = text.replace(/\*\*/g, '');
50
-
51
- // Remove any empty lines
52
- cleanedText = cleanedText.replace(/\n\s*\n/g, '\n\n');
53
-
54
- // Remove any trailing whitespace
55
- return cleanedText.trim();
56
- } catch (error) {
57
- console.error('Error formatting Monica response:', error.message);
58
- return text.trim(); // Return original if formatting fails
59
- }
60
- }
61
-
62
- async search(prompt) {
63
- // Input validation
64
- if (!prompt || typeof prompt !== 'string') {
65
- throw new Error('Invalid prompt: must be a non-empty string');
66
- }
67
-
68
- if (prompt.length > 5000) {
69
- throw new Error('Invalid prompt: too long (maximum 5000 characters)');
70
- }
71
-
72
- const taskId = randomUUID();
73
- const payload = {
74
- "pro": false,
75
- "query": prompt,
76
- "round": 1,
77
- "session_id": this.sessionId,
78
- "language": "auto",
79
- "task_id": taskId
80
- };
81
-
82
- const cookies = {
83
- "monica_home_theme": "auto"
84
- };
85
-
86
- // Convert cookies object to string
87
- const cookieString = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
88
-
89
- try {
90
- console.log(`Monica API request starting: "${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}"`);
91
-
92
- const response = await this.client.post(this.apiEndpoint, payload, {
93
- headers: {
94
- ...this.headers,
95
- 'Cookie': cookieString
96
- },
97
- responseType: 'stream',
98
- validateStatus: function (status) {
99
- return status < 500; // Accept non-error responses
100
- }
101
- });
102
-
103
- let fullText = '';
104
- let receivedData = false;
105
-
106
- return new Promise((resolve, reject) => {
107
- const timeoutId = setTimeout(() => {
108
- reject(new Error('Monica stream timeout: no response data received'));
109
- }, this.timeout);
110
-
111
- response.data.on('data', (chunk) => {
112
- receivedData = true;
113
- const lines = chunk.toString().split('\n');
114
-
115
- for (const line of lines) {
116
- if (line.startsWith('data: ')) {
117
- try {
118
- const jsonStr = line.substring(6);
119
- const data = JSON.parse(jsonStr);
120
-
121
- if (data.session_id) {
122
- this.sessionId = data.session_id;
123
- }
124
-
125
- if (data.text) {
126
- fullText += data.text;
127
- }
128
-
129
- console.log('Monica data chunk received:', data.text?.substring(0, 50) + '...');
130
- } catch (e) {
131
- // Ignore parse errors for non-JSON lines
132
- console.debug('Ignoring non-JSON line:', line.substring(0, 50));
133
- }
134
- }
135
- }
136
- });
137
-
138
- response.data.on('end', () => {
139
- clearTimeout(timeoutId);
140
-
141
- if (!receivedData) {
142
- reject(new Error('Monica no data received: empty response'));
143
- return;
144
- }
145
-
146
- console.log('Monica stream completed, total length:', fullText.length);
147
-
148
- const formatted = this.formatResponse(fullText);
149
-
150
- if (!formatted || formatted.trim() === '') {
151
- reject(new Error('Monica no valid content: received empty or invalid response'));
152
- return;
153
- }
154
-
155
- resolve(formatted);
156
- });
157
-
158
- response.data.on('error', (err) => {
159
- clearTimeout(timeoutId);
160
- console.error('Monica stream error:', err.message);
161
-
162
- if (err.code === 'ENOTFOUND') {
163
- reject(new Error('Monica network error: unable to resolve host'));
164
- } else if (err.code === 'ECONNREFUSED') {
165
- reject(new Error('Monica network error: connection refused'));
166
- } else {
167
- reject(new Error(`Monica stream error: ${err.message}`));
168
- }
169
- });
170
- });
171
-
172
- } catch (error) {
173
- console.error('Monica API request failed:', error.message);
174
-
175
- if (error.response) {
176
- // HTTP error response
177
- const status = error.response.status;
178
- if (status === 429) {
179
- throw new Error('Monica rate limit: too many requests');
180
- } else if (status >= 500) {
181
- throw new Error(`Monica server error: HTTP ${status}`);
182
- } else if (status >= 400) {
183
- throw new Error(`Monica client error: HTTP ${status}`);
184
- }
185
- }
186
-
187
- if (error.code === 'ECONNABORTED') {
188
- throw new Error('Monica request timeout: took too long');
189
- }
190
-
191
- throw new Error(`Monica API request failed: ${error.message}`);
192
- }
193
- }
194
- }
195
-
196
- /**
197
- * Search using Monica AI
198
- * @param {string} query - The search query
199
- * @returns {Promise<string>} The search results
200
- */
201
- export async function searchMonica(query) {
202
- // Input validation
203
- if (!query || typeof query !== 'string') {
204
- throw new Error('Invalid query: query must be a non-empty string');
205
- }
206
-
207
- console.log(`Monica AI search starting: "${query}"`);
208
-
209
- try {
210
- const client = new MonicaClient();
211
- const result = await client.search(query);
212
-
213
- if (result && result.trim()) {
214
- console.log(`Monica AI search completed: ${result.length} characters received`);
215
- } else {
216
- console.log('Monica AI search completed but returned empty result');
217
- }
218
-
219
- return result;
220
- } catch (error) {
221
- console.error('Error in Monica AI search:', error.message);
222
-
223
- // Enhanced error handling
224
- if (error.code === 'ENOTFOUND') {
225
- throw new Error('Monica network error: unable to resolve host');
226
- }
227
-
228
- if (error.code === 'ECONNREFUSED') {
229
- throw new Error('Monica network error: connection refused');
230
- }
231
-
232
- if (error.message.includes('timeout')) {
233
- throw new Error('Monica timeout: request took too long');
234
- }
235
-
236
- if (error.message.includes('network')) {
237
- throw new Error('Monica network error: service may be unavailable');
238
- }
239
-
240
- throw new Error(`Monica search failed for "${query}": ${error.message}`);
241
- }
242
- }
5
+ class MonicaClient {
6
+ constructor(timeout = 60000) {
7
+ this.apiEndpoint = "https://monica.so/api/search_v1/search";
8
+ this.timeout = timeout;
9
+ this.clientId = randomUUID();
10
+ this.sessionId = "";
11
+
12
+ this.headers = {
13
+ "accept": "*/*",
14
+ "accept-encoding": "gzip, deflate, br, zstd",
15
+ "accept-language": "en-US,en;q=0.9",
16
+ "content-type": "application/json",
17
+ "dnt": "1",
18
+ "origin": "https://monica.so",
19
+ "referer": "https://monica.so/answers",
20
+ "sec-ch-ua": '"Microsoft Edge";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
21
+ "sec-ch-ua-mobile": "?0",
22
+ "sec-ch-ua-platform": '"Windows"',
23
+ "sec-fetch-dest": "empty",
24
+ "sec-fetch-mode": "cors",
25
+ "sec-fetch-site": "same-origin",
26
+ "sec-gpc": "1",
27
+ "user-agent": getRandomUserAgent(),
28
+ "x-client-id": this.clientId,
29
+ "x-client-locale": "en",
30
+ "x-client-type": "web",
31
+ "x-client-version": "5.4.3",
32
+ "x-from-channel": "NA",
33
+ "x-product-name": "Monica-Search",
34
+ "x-time-zone": "Asia/Calcutta;-330"
35
+ };
36
+
37
+ // Axios instance with improved configuration
38
+ this.client = axios.create({
39
+ headers: this.headers,
40
+ timeout: this.timeout,
41
+ withCredentials: true,
42
+ validateStatus: (status) => status >= 200 && status < 500 // Accept non-error status codes
43
+ });
44
+ }
45
+
46
+ formatResponse(text) {
47
+ try {
48
+ // Clean up markdown formatting
49
+ let cleanedText = text.replace(/\*\*/g, '');
50
+
51
+ // Remove any empty lines
52
+ cleanedText = cleanedText.replace(/\n\s*\n/g, '\n\n');
53
+
54
+ // Remove any trailing whitespace
55
+ return cleanedText.trim();
56
+ } catch (error) {
57
+ console.error('Error formatting Monica response:', error.message);
58
+ return text.trim(); // Return original if formatting fails
59
+ }
60
+ }
61
+
62
+ async search(prompt) {
63
+ // Input validation
64
+ if (!prompt || typeof prompt !== 'string') {
65
+ throw new Error('Invalid prompt: must be a non-empty string');
66
+ }
67
+
68
+ if (prompt.length > 5000) {
69
+ throw new Error('Invalid prompt: too long (maximum 5000 characters)');
70
+ }
71
+
72
+ const taskId = randomUUID();
73
+ const payload = {
74
+ "pro": false,
75
+ "query": prompt,
76
+ "round": 1,
77
+ "session_id": this.sessionId,
78
+ "language": "auto",
79
+ "task_id": taskId
80
+ };
81
+
82
+ const cookies = {
83
+ "monica_home_theme": "auto"
84
+ };
85
+
86
+ // Convert cookies object to string
87
+ const cookieString = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
88
+
89
+ try {
90
+ console.log(`Monica API request starting: "${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}"`);
91
+
92
+ const response = await this.client.post(this.apiEndpoint, payload, {
93
+ headers: {
94
+ ...this.headers,
95
+ 'Cookie': cookieString
96
+ },
97
+ responseType: 'stream',
98
+ validateStatus: function (status) {
99
+ return status < 500; // Accept non-error responses
100
+ }
101
+ });
102
+
103
+ let fullText = '';
104
+ let receivedData = false;
105
+
106
+ return new Promise((resolve, reject) => {
107
+ const timeoutId = setTimeout(() => {
108
+ reject(new Error('Monica stream timeout: no response data received'));
109
+ }, this.timeout);
110
+
111
+ response.data.on('data', (chunk) => {
112
+ receivedData = true;
113
+ const lines = chunk.toString().split('\n');
114
+
115
+ for (const line of lines) {
116
+ if (line.startsWith('data: ')) {
117
+ try {
118
+ const jsonStr = line.substring(6);
119
+ const data = JSON.parse(jsonStr);
120
+
121
+ if (data.session_id) {
122
+ this.sessionId = data.session_id;
123
+ }
124
+
125
+ if (data.text) {
126
+ fullText += data.text;
127
+ }
128
+
129
+ console.log('Monica data chunk received:', data.text?.substring(0, 50) + '...');
130
+ } catch (e) {
131
+ // Ignore parse errors for non-JSON lines
132
+ console.debug('Ignoring non-JSON line:', line.substring(0, 50));
133
+ }
134
+ }
135
+ }
136
+ });
137
+
138
+ response.data.on('end', () => {
139
+ clearTimeout(timeoutId);
140
+
141
+ if (!receivedData) {
142
+ reject(new Error('Monica no data received: empty response'));
143
+ return;
144
+ }
145
+
146
+ console.log('Monica stream completed, total length:', fullText.length);
147
+
148
+ const formatted = this.formatResponse(fullText);
149
+
150
+ if (!formatted || formatted.trim() === '') {
151
+ reject(new Error('Monica no valid content: received empty or invalid response'));
152
+ return;
153
+ }
154
+
155
+ resolve(formatted);
156
+ });
157
+
158
+ response.data.on('error', (err) => {
159
+ clearTimeout(timeoutId);
160
+ console.error('Monica stream error:', err.message);
161
+
162
+ if (err.code === 'ENOTFOUND') {
163
+ reject(new Error('Monica network error: unable to resolve host'));
164
+ } else if (err.code === 'ECONNREFUSED') {
165
+ reject(new Error('Monica network error: connection refused'));
166
+ } else {
167
+ reject(new Error(`Monica stream error: ${err.message}`));
168
+ }
169
+ });
170
+ });
171
+
172
+ } catch (error) {
173
+ console.error('Monica API request failed:', error.message);
174
+
175
+ if (error.response) {
176
+ // HTTP error response
177
+ const status = error.response.status;
178
+ if (status === 429) {
179
+ throw new Error('Monica rate limit: too many requests');
180
+ } else if (status >= 500) {
181
+ throw new Error(`Monica server error: HTTP ${status}`);
182
+ } else if (status >= 400) {
183
+ throw new Error(`Monica client error: HTTP ${status}`);
184
+ }
185
+ }
186
+
187
+ if (error.code === 'ECONNABORTED') {
188
+ throw new Error('Monica request timeout: took too long');
189
+ }
190
+
191
+ throw new Error(`Monica API request failed: ${error.message}`);
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Search using Monica AI
198
+ * @param {string} query - The search query
199
+ * @returns {Promise<string>} The search results
200
+ */
201
+ export async function searchMonica(query) {
202
+ // Input validation
203
+ if (!query || typeof query !== 'string') {
204
+ throw new Error('Invalid query: query must be a non-empty string');
205
+ }
206
+
207
+ console.log(`Monica AI search starting: "${query}"`);
208
+
209
+ try {
210
+ const client = new MonicaClient();
211
+ const result = await client.search(query);
212
+
213
+ if (result && result.trim()) {
214
+ console.log(`Monica AI search completed: ${result.length} characters received`);
215
+ } else {
216
+ console.log('Monica AI search completed but returned empty result');
217
+ }
218
+
219
+ return result;
220
+ } catch (error) {
221
+ console.error('Error in Monica AI search:', error.message);
222
+
223
+ // Enhanced error handling
224
+ if (error.code === 'ENOTFOUND') {
225
+ throw new Error('Monica network error: unable to resolve host');
226
+ }
227
+
228
+ if (error.code === 'ECONNREFUSED') {
229
+ throw new Error('Monica network error: connection refused');
230
+ }
231
+
232
+ if (error.message.includes('timeout')) {
233
+ throw new Error('Monica timeout: request took too long');
234
+ }
235
+
236
+ if (error.message.includes('network')) {
237
+ throw new Error('Monica network error: service may be unavailable');
238
+ }
239
+
240
+ throw new Error(`Monica search failed for "${query}": ${error.message}`);
241
+ }
242
+ }
package/test.setup.js CHANGED
@@ -1,120 +1,73 @@
1
- // Global test setup
2
- import { jest } from '@jest/globals';
3
-
4
- // Mock console methods to reduce noise in tests
5
- global.console = {
6
- ...console,
7
- log: jest.fn(),
8
- error: jest.fn(),
9
- warn: jest.fn(),
10
- info: jest.fn(),
11
- debug: jest.fn()
12
- };
13
-
14
- // Increase test timeout for integration tests
15
- jest.setTimeout(30000);
16
-
17
- // Global test utilities
18
- global.mockDelay = (ms = 100) => new Promise(resolve => setTimeout(resolve, ms));
19
-
20
- // Mock WebSocket class
21
- global.WebSocket = jest.fn().mockImplementation(() => ({
22
- close: jest.fn(),
23
- send: jest.fn(),
24
- addEventListener: jest.fn(),
25
- removeEventListener: jest.fn()
26
- }));
27
-
28
- // Mock axios
29
- const mockAxios = jest.fn(() => Promise.resolve({
30
- status: 200,
31
- data: '<html><body>Mock Response</body></html>',
32
- config: { url: 'http://example.com' },
33
- request: { res: { responseUrl: 'http://example.com' } }
34
- }));
35
-
36
- mockAxios.get = jest.fn();
37
- mockAxios.post = jest.fn();
38
-
39
- jest.mock('axios', () => mockAxios);
40
-
41
- // Mock external modules
42
- jest.mock('cheerio', () => ({
43
- load: jest.fn(() => ({
44
- find: jest.fn(() => ({
45
- each: jest.fn(),
46
- text: jest.fn(() => 'Mock Title'),
47
- attr: jest.fn(() => 'http://example.com')
48
- })),
49
- html: jest.fn(() => '<div>Mock Content</div>'),
50
- text: jest.fn(() => 'Mock Text Content')
51
- }))
52
- }));
53
-
54
- jest.mock('ws', () => jest.fn());
55
-
56
- jest.mock('turndown', () => jest.fn(() => ({
57
- turndown: jest.fn((html) => html.replace(/<[^>]*>/g, ''))
58
- })));
59
-
60
- jest.mock('tough-cookie', () => ({
61
- CookieJar: jest.fn(() => ({
62
- getCookies: jest.fn(() => []),
63
- setCookie: jest.fn()
64
- }))
65
- }));
66
-
67
- jest.mock('axios-cookiejar-support', () => ({
68
- wrapper: jest.fn((axios) => axios)
69
- }));
70
-
71
- jest.mock('crypto', () => ({
72
- randomUUID: jest.fn(() => 'mock-uuid-12345')
73
- }));
74
-
75
- // Mock axios
76
- const mockAxios = jest.fn(() => Promise.resolve({
77
- status: 200,
78
- data: '<html><body>Mock Response</body></html>',
79
- config: { url: 'http://example.com' },
80
- request: { res: { responseUrl: 'http://example.com' } }
81
- }));
82
-
83
- mockAxios.get = jest.fn();
84
- mockAxios.post = jest.fn();
85
-
86
- jest.mock('axios', () => mockAxios);
87
-
88
- // Mock external modules
89
- jest.mock('cheerio', () => ({
90
- load: jest.fn(() => ({
91
- find: jest.fn(() => ({
92
- each: jest.fn(),
93
- text: jest.fn(() => 'Mock Title'),
94
- attr: jest.fn(() => 'http://example.com')
95
- })),
96
- html: jest.fn(() => '<div>Mock Content</div>'),
97
- text: jest.fn(() => 'Mock Text Content')
98
- }))
99
- }));
100
-
101
- jest.mock('ws', () => jest.fn());
102
-
103
- jest.mock('turndown', () => jest.fn(() => ({
104
- turndown: jest.fn((html) => html.replace(/<[^>]*>/g, ''))
105
- })));
106
-
107
- jest.mock('tough-cookie', () => ({
108
- CookieJar: jest.fn(() => ({
109
- getCookies: jest.fn(() => []),
110
- setCookie: jest.fn()
111
- }))
112
- }));
113
-
114
- jest.mock('axios-cookiejar-support', () => ({
115
- wrapper: jest.fn((axios) => axios)
116
- }));
117
-
118
- jest.mock('crypto', () => ({
119
- randomUUID: jest.fn(() => 'mock-uuid-12345')
1
+ // Global test setup
2
+ import { jest } from '@jest/globals';
3
+
4
+ // Mock console methods to reduce noise in tests
5
+ global.console = {
6
+ ...console,
7
+ log: jest.fn(),
8
+ error: jest.fn(),
9
+ warn: jest.fn(),
10
+ info: jest.fn(),
11
+ debug: jest.fn()
12
+ };
13
+
14
+ // Increase test timeout for integration tests
15
+ jest.setTimeout(30000);
16
+
17
+ // Global test utilities
18
+ global.mockDelay = (ms = 100) => new Promise(resolve => setTimeout(resolve, ms));
19
+
20
+ // Mock WebSocket class
21
+ global.WebSocket = jest.fn().mockImplementation(() => ({
22
+ close: jest.fn(),
23
+ send: jest.fn(),
24
+ addEventListener: jest.fn(),
25
+ removeEventListener: jest.fn()
26
+ }));
27
+
28
+ // Mock axios
29
+ const mockAxios = jest.fn(() => Promise.resolve({
30
+ status: 200,
31
+ data: '<html><body>Mock Response</body></html>',
32
+ config: { url: 'http://example.com' },
33
+ request: { res: { responseUrl: 'http://example.com' } }
34
+ }));
35
+
36
+ mockAxios.get = jest.fn();
37
+ mockAxios.post = jest.fn();
38
+
39
+ jest.mock('axios', () => mockAxios);
40
+
41
+ // Mock external modules
42
+ jest.mock('cheerio', () => ({
43
+ load: jest.fn(() => ({
44
+ find: jest.fn(() => ({
45
+ each: jest.fn(),
46
+ text: jest.fn(() => 'Mock Title'),
47
+ attr: jest.fn(() => 'http://example.com')
48
+ })),
49
+ html: jest.fn(() => '<div>Mock Content</div>'),
50
+ text: jest.fn(() => 'Mock Text Content')
51
+ }))
52
+ }));
53
+
54
+ jest.mock('ws', () => jest.fn());
55
+
56
+ jest.mock('turndown', () => jest.fn(() => ({
57
+ turndown: jest.fn((html) => html.replace(/<[^>]*>/g, ''))
58
+ })));
59
+
60
+ jest.mock('tough-cookie', () => ({
61
+ CookieJar: jest.fn(() => ({
62
+ getCookies: jest.fn(() => []),
63
+ setCookie: jest.fn()
64
+ }))
65
+ }));
66
+
67
+ jest.mock('axios-cookiejar-support', () => ({
68
+ wrapper: jest.fn((axios) => axios)
69
+ }));
70
+
71
+ jest.mock('crypto', () => ({
72
+ randomUUID: jest.fn(() => 'mock-uuid-12345')
120
73
  }));