@vinitngr/serper-v 1.0.1 → 1.0.3
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.
- package/SKILL.md +14 -8
- package/dist/index.js +110 -25
- package/package.json +3 -2
package/SKILL.md
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: serper
|
|
3
|
-
description:
|
|
3
|
+
description: Advanced search (organic, places, news, scholar, patents) and bulk scraping via Serper API.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Serper Search
|
|
7
7
|
|
|
8
8
|
## Usage
|
|
9
9
|
```bash
|
|
10
|
-
serperV search --query "Term" --type [type] --limit 10
|
|
10
|
+
serperV search --query "Term" --type [type] --limit 10 --tbs qdr:h
|
|
11
|
+
serperV scrape --url "url1, url2"
|
|
11
12
|
```
|
|
12
|
-
|
|
13
|
-
- **Types**: `search` (default), `
|
|
13
|
+
|
|
14
|
+
- **Types**: `search` (default), `places`, `maps`, `news`, `shopping`, `scholar`, `patents`.
|
|
15
|
+
- **Date Range (`--tbs`)**: `qdr:h` (hour), `qdr:d` (day), `qdr:w` (week), `qdr:m` (month), `qdr:y` (year).
|
|
14
16
|
- **Flags**:
|
|
15
|
-
- `-q` / `--query`: Search
|
|
16
|
-
- `-
|
|
17
|
+
- `-q` / `--query`: Search query.
|
|
18
|
+
- `-u` / `--url`: Comma-separated URLs for bulk scraping.
|
|
19
|
+
- `-t` / `--type`: Endpoint selection.
|
|
17
20
|
- `-l` / `--limit`: Number of results (max 100).
|
|
18
|
-
- `-g` / `--gl`: Country code
|
|
19
|
-
- `-h` / `--hl`: Language code
|
|
21
|
+
- `-g` / `--gl`: Country code.
|
|
22
|
+
- `-h` / `--hl`: Language code.
|
|
20
23
|
- `-p` / `--page`: Pagination number.
|
|
21
24
|
|
|
22
25
|
## Installation
|
|
23
26
|
`npm install -g @vinitngr/serper-v --force`
|
|
27
|
+
|
|
28
|
+
## Auth
|
|
29
|
+
`serperV auth <key>` (saves to `~/.vinit/credentials.json`) or `export SERPER_API_KEY=key`.
|
package/dist/index.js
CHANGED
|
@@ -18,12 +18,14 @@ function getStoredApiKey() {
|
|
|
18
18
|
function parseArgs(args) {
|
|
19
19
|
const options = {
|
|
20
20
|
query: '',
|
|
21
|
+
url: '',
|
|
21
22
|
limit: 10,
|
|
22
23
|
type: 'search',
|
|
23
24
|
gl: 'us',
|
|
24
25
|
hl: 'en',
|
|
25
26
|
page: 1,
|
|
26
|
-
autocorrect: true
|
|
27
|
+
autocorrect: true,
|
|
28
|
+
tbs: ''
|
|
27
29
|
};
|
|
28
30
|
for (let i = 0; i < args.length; i++) {
|
|
29
31
|
const arg = args[i];
|
|
@@ -32,6 +34,10 @@ function parseArgs(args) {
|
|
|
32
34
|
options.query = nextArg;
|
|
33
35
|
i++;
|
|
34
36
|
}
|
|
37
|
+
else if (arg === '-u' || arg === '--url') {
|
|
38
|
+
options.url = nextArg;
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
35
41
|
else if (arg === '-l' || arg === '--limit') {
|
|
36
42
|
options.limit = parseInt(nextArg);
|
|
37
43
|
i++;
|
|
@@ -56,11 +62,16 @@ function parseArgs(args) {
|
|
|
56
62
|
options.autocorrect = nextArg === 'true';
|
|
57
63
|
i++;
|
|
58
64
|
}
|
|
65
|
+
else if (arg === '--tbs') {
|
|
66
|
+
options.tbs = nextArg;
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
59
69
|
}
|
|
60
70
|
return options;
|
|
61
71
|
}
|
|
62
72
|
const args = process.argv.slice(2);
|
|
63
73
|
const command = args[0];
|
|
74
|
+
const apiKey = process.env.SERPER_API_KEY || getStoredApiKey();
|
|
64
75
|
if (command === 'auth') {
|
|
65
76
|
const key = args[1];
|
|
66
77
|
if (!key) {
|
|
@@ -86,25 +97,112 @@ else if (command === 'search') {
|
|
|
86
97
|
console.error('\n❌ Error: Query required. Use -q or --query\n');
|
|
87
98
|
process.exit(1);
|
|
88
99
|
}
|
|
89
|
-
const apiKey = process.env.SERPER_API_KEY || getStoredApiKey();
|
|
90
100
|
if (!apiKey) {
|
|
91
101
|
console.error('\n❌ Error: Serper API key not found. Run: serperV auth <key>\n');
|
|
92
102
|
process.exit(1);
|
|
93
103
|
}
|
|
94
|
-
const
|
|
104
|
+
const payload = {
|
|
95
105
|
q: options.query,
|
|
96
106
|
num: options.limit,
|
|
97
107
|
gl: options.gl,
|
|
98
108
|
hl: options.hl,
|
|
99
109
|
page: options.page,
|
|
100
110
|
autocorrect: options.autocorrect
|
|
101
|
-
}
|
|
111
|
+
};
|
|
112
|
+
if (options.tbs)
|
|
113
|
+
payload.tbs = options.tbs;
|
|
114
|
+
makeRequest(`/${options.type}`, JSON.stringify(payload));
|
|
115
|
+
}
|
|
116
|
+
else if (command === 'scrape') {
|
|
117
|
+
const options = parseArgs(args.slice(1));
|
|
118
|
+
if (!options.url) {
|
|
119
|
+
console.error('\n❌ Error: URL required. Use -u or --url\n');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
if (!apiKey) {
|
|
123
|
+
console.error('\n❌ Error: Serper API key not found. Run: serperV auth <key>\n');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const urls = options.url.split(',').map((u) => u.trim());
|
|
127
|
+
if (urls.length === 1) {
|
|
128
|
+
makeRequest('/scrape', JSON.stringify({ url: urls[0] }));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Bulk scraping
|
|
132
|
+
process.stdout.write('[\n');
|
|
133
|
+
const processNext = (index) => {
|
|
134
|
+
if (index >= urls.length) {
|
|
135
|
+
process.stdout.write('\n]\n');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const data = JSON.stringify({ url: urls[index] });
|
|
139
|
+
const req = https.request({
|
|
140
|
+
hostname: 'google.serper.dev',
|
|
141
|
+
path: '/scrape',
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: {
|
|
144
|
+
'X-API-KEY': apiKey || '',
|
|
145
|
+
'Content-Type': 'application/json',
|
|
146
|
+
'Content-Length': data.length
|
|
147
|
+
}
|
|
148
|
+
}, (res) => {
|
|
149
|
+
let body = '';
|
|
150
|
+
res.on('data', (chunk) => body += chunk);
|
|
151
|
+
res.on('end', () => {
|
|
152
|
+
try {
|
|
153
|
+
process.stdout.write(JSON.stringify(JSON.parse(body), null, 2));
|
|
154
|
+
if (index < urls.length - 1)
|
|
155
|
+
process.stdout.write(',\n');
|
|
156
|
+
processNext(index + 1);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.error(`\n❌ Error parsing response for ${urls[index]}`);
|
|
160
|
+
processNext(index + 1);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
req.on('error', (err) => {
|
|
165
|
+
console.error(`\n❌ Request failed for ${urls[index]}:`, err.message);
|
|
166
|
+
processNext(index + 1);
|
|
167
|
+
});
|
|
168
|
+
req.write(data);
|
|
169
|
+
req.end();
|
|
170
|
+
};
|
|
171
|
+
processNext(0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(`
|
|
176
|
+
Optimal Serper CLI (Zero Dependencies) - v2.4.0
|
|
177
|
+
|
|
178
|
+
Usage:
|
|
179
|
+
serperV auth <key> Store API key
|
|
180
|
+
serperV search [options] Search (places, news, scholar, etc.)
|
|
181
|
+
serperV scrape [options] Scrape content (one or more URLs)
|
|
182
|
+
|
|
183
|
+
Options:
|
|
184
|
+
-q, --query <string> Search query
|
|
185
|
+
-u, --url <string> URL(s) to scrape (comma-separated for bulk)
|
|
186
|
+
-t, --type <string> Endpoint (search, places, maps, news, shopping, scholar, patents)
|
|
187
|
+
-l, --limit <number> Result limit (Default 10)
|
|
188
|
+
-g, --gl <string> Country code (Default 'us')
|
|
189
|
+
-h, --hl <string> Language code (Default 'en')
|
|
190
|
+
-p, --page <number> Page number
|
|
191
|
+
-a, --autocorrect <bool>
|
|
192
|
+
--tbs <string> Date range (e.g. qdr:h, qdr:d, qdr:w, qdr:m, qdr:y)
|
|
193
|
+
|
|
194
|
+
Examples:
|
|
195
|
+
serperV search -q "Apple" --tbs qdr:h
|
|
196
|
+
serperV scrape -u "https://google.com, https://apple.com"
|
|
197
|
+
`);
|
|
198
|
+
}
|
|
199
|
+
function makeRequest(path, data) {
|
|
102
200
|
const reqOptions = {
|
|
103
201
|
hostname: 'google.serper.dev',
|
|
104
|
-
path:
|
|
202
|
+
path: path,
|
|
105
203
|
method: 'POST',
|
|
106
204
|
headers: {
|
|
107
|
-
'X-API-KEY': apiKey,
|
|
205
|
+
'X-API-KEY': apiKey || '',
|
|
108
206
|
'Content-Type': 'application/json',
|
|
109
207
|
'Content-Length': data.length
|
|
110
208
|
}
|
|
@@ -114,7 +212,12 @@ else if (command === 'search') {
|
|
|
114
212
|
res.on('data', (chunk) => body += chunk);
|
|
115
213
|
res.on('end', () => {
|
|
116
214
|
if (res.statusCode === 200) {
|
|
117
|
-
|
|
215
|
+
try {
|
|
216
|
+
process.stdout.write(JSON.stringify(JSON.parse(body), null, 2) + '\n');
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
process.stdout.write(body + '\n');
|
|
220
|
+
}
|
|
118
221
|
}
|
|
119
222
|
else {
|
|
120
223
|
const errorMsg = res.statusCode === 401 || res.statusCode === 403
|
|
@@ -134,21 +237,3 @@ else if (command === 'search') {
|
|
|
134
237
|
req.write(data);
|
|
135
238
|
req.end();
|
|
136
239
|
}
|
|
137
|
-
else {
|
|
138
|
-
console.log(`
|
|
139
|
-
Optimal Serper CLI (Zero Dependencies) - v2.2.0
|
|
140
|
-
|
|
141
|
-
Usage:
|
|
142
|
-
serperV auth <key> Store API key
|
|
143
|
-
serperV search [options] Search the web
|
|
144
|
-
|
|
145
|
-
Options:
|
|
146
|
-
-q, --query <string> Search query (Required)
|
|
147
|
-
-l, --limit <number> Result limit (Default 10)
|
|
148
|
-
-t, --type <string> Type (search, news, places, etc.)
|
|
149
|
-
-g, --gl <string> Country code (Default 'us')
|
|
150
|
-
-h, --hl <string> Language code (Default 'en')
|
|
151
|
-
-p, --page <number> Page number
|
|
152
|
-
-a, --autocorrect <bool>
|
|
153
|
-
`);
|
|
154
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vinitngr/serper-v",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Optimal Serper Search CLI for AI Agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc && chmod +x dist/index.js",
|
|
16
16
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
17
|
-
"test:search": "node test-serper.js"
|
|
17
|
+
"test:search": "node test-serper.js",
|
|
18
|
+
"test:all": "node test-all.js"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|
|
20
21
|
"serper",
|