summit-search-cli 1.0.0
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/README.md +48 -0
- package/package.json +39 -0
- package/src/duckduckgo.js +88 -0
- package/src/extract.js +62 -0
- package/src/fetch.js +25 -0
- package/src/generator.js +331 -0
- package/src/index.js +44 -0
- package/src/output.js +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Search CLI
|
|
2
|
+
|
|
3
|
+
Search CLI is a terminal-based web search and information retrieval tool designed to bring useful answers directly into your command line.
|
|
4
|
+
|
|
5
|
+
Instead of only returning a list of links, Search searches the web, reads available sources, extracts important information, removes unnecessary page content, and ranks useful sentences to create a cleaner human-readable response.
|
|
6
|
+
|
|
7
|
+
Search is built by **Summit** as part of its collection of developer tools focused on making powerful workflows available directly from the terminal.
|
|
8
|
+
|
|
9
|
+
## Why Search Exists
|
|
10
|
+
|
|
11
|
+
Traditional command-line search tools usually stop at showing URLs. While that can be useful, it often requires opening multiple browser tabs, reading through pages, and manually finding the important information.
|
|
12
|
+
|
|
13
|
+
Search was created to reduce that process by bringing the information itself into the terminal.
|
|
14
|
+
|
|
15
|
+
The goal is not to replace the web, but to make finding and understanding information faster.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- ๐ DuckDuckGo-powered web searching
|
|
20
|
+
- ๐ Browserless information lookup
|
|
21
|
+
- ๐ Automatic web page content extraction
|
|
22
|
+
- ๐งน Removes unnecessary website clutter
|
|
23
|
+
- ๐ง Smart sentence ranking system
|
|
24
|
+
- โจ Duplicate information removal
|
|
25
|
+
- ๐ Combines information from multiple sources
|
|
26
|
+
- ๐จ Clean colored terminal output
|
|
27
|
+
- โก Designed for fast command-line workflows
|
|
28
|
+
|
|
29
|
+
## How It Works
|
|
30
|
+
|
|
31
|
+
Search uses a multi-step pipeline:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
User Query
|
|
35
|
+
โ
|
|
36
|
+
DuckDuckGo Search
|
|
37
|
+
โ
|
|
38
|
+
Source Collection
|
|
39
|
+
โ
|
|
40
|
+
Web Page Fetching
|
|
41
|
+
โ
|
|
42
|
+
Content Extraction
|
|
43
|
+
โ
|
|
44
|
+
Text Cleaning
|
|
45
|
+
โ
|
|
46
|
+
Sentence Ranking
|
|
47
|
+
โ
|
|
48
|
+
Readable Terminal Response
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "summit-search-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A terminal-based search CLI that uses DuckDuckGo to find and display web results directly from the command line.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"search",
|
|
7
|
+
"cli",
|
|
8
|
+
"terminal",
|
|
9
|
+
"command-line",
|
|
10
|
+
"web-search",
|
|
11
|
+
"duckduckgo",
|
|
12
|
+
"browserless",
|
|
13
|
+
"developer-tools",
|
|
14
|
+
"productivity",
|
|
15
|
+
"utility",
|
|
16
|
+
"nodejs",
|
|
17
|
+
"npm",
|
|
18
|
+
"automation",
|
|
19
|
+
"research",
|
|
20
|
+
"documentation",
|
|
21
|
+
"ai-tools",
|
|
22
|
+
"bashlife",
|
|
23
|
+
"summit"
|
|
24
|
+
],
|
|
25
|
+
"license": "GPL-2.0",
|
|
26
|
+
"author": "summit",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "src/index.js",
|
|
29
|
+
"bin": {
|
|
30
|
+
"search": "./src/index.js"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"start": "node src/index.js"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"chalk": "^5.6.2",
|
|
37
|
+
"cheerio": "^1.2.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import https from "https";
|
|
2
|
+
|
|
3
|
+
function request(url) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
https.get(
|
|
6
|
+
url,
|
|
7
|
+
{
|
|
8
|
+
headers: {
|
|
9
|
+
"User-Agent": "Mozilla/5.0"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
(res) => {
|
|
13
|
+
let data = "";
|
|
14
|
+
|
|
15
|
+
res.on("data", chunk => {
|
|
16
|
+
data += chunk;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
res.on("end", () => {
|
|
20
|
+
resolve(data);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
).on("error", reject);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
function decodeUrl(url) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(
|
|
31
|
+
url.startsWith("http")
|
|
32
|
+
? url
|
|
33
|
+
: "https:" + url
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const target = parsed.searchParams.get("uddg");
|
|
37
|
+
|
|
38
|
+
if (target) {
|
|
39
|
+
return decodeURIComponent(target);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return url;
|
|
43
|
+
|
|
44
|
+
} catch {
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
export async function duckSearch(query) {
|
|
51
|
+
|
|
52
|
+
const url =
|
|
53
|
+
"https://html.duckduckgo.com/html/?q=" +
|
|
54
|
+
encodeURIComponent(query);
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
const html = await request(url);
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
const results = [];
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
const regex =
|
|
64
|
+
/result__a[^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/g;
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
let match;
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
while (
|
|
71
|
+
(match = regex.exec(html)) &&
|
|
72
|
+
results.length < 5
|
|
73
|
+
) {
|
|
74
|
+
|
|
75
|
+
results.push({
|
|
76
|
+
title: match[2]
|
|
77
|
+
.replace(/<[^>]+>/g, "")
|
|
78
|
+
.replace(/&/g, "&")
|
|
79
|
+
.trim(),
|
|
80
|
+
|
|
81
|
+
url: decodeUrl(match[1])
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
return results;
|
|
88
|
+
}
|
package/src/extract.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as cheerio from "cheerio";
|
|
2
|
+
|
|
3
|
+
export function extractText(html) {
|
|
4
|
+
const $ = cheerio.load(html);
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
$(
|
|
8
|
+
"script, style, nav, header, footer, aside, " +
|
|
9
|
+
"button, form, svg, noscript, " +
|
|
10
|
+
"table, sup, .reference, .mw-editsection, " +
|
|
11
|
+
".infobox, .sidebar, .metadata"
|
|
12
|
+
).remove();
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
let paragraphs = [];
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
$("p").each((_, el) => {
|
|
19
|
+
const text = $(el)
|
|
20
|
+
.text()
|
|
21
|
+
.replace(/\s+/g, " ")
|
|
22
|
+
.trim();
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if (text.length > 50) {
|
|
26
|
+
paragraphs.push(text);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
let text = paragraphs.join(" ");
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
text = text
|
|
35
|
+
.replace(/\[\d+\]/g, "")
|
|
36
|
+
.replace(/โ/g, "")
|
|
37
|
+
.replace(/\([^)]*citation[^)]*\)/gi, "")
|
|
38
|
+
.replace(/\s+/g, " ")
|
|
39
|
+
.trim();
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const junk = [
|
|
43
|
+
"skip to content",
|
|
44
|
+
"cookie policy",
|
|
45
|
+
"privacy policy",
|
|
46
|
+
"terms of service",
|
|
47
|
+
"sign in",
|
|
48
|
+
"log in",
|
|
49
|
+
"subscribe"
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
for (const item of junk) {
|
|
54
|
+
text = text.replace(
|
|
55
|
+
new RegExp(item, "gi"),
|
|
56
|
+
""
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
return text;
|
|
62
|
+
}
|
package/src/fetch.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import https from "https";
|
|
2
|
+
|
|
3
|
+
export function fetchPage(url) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
https.get(
|
|
6
|
+
url,
|
|
7
|
+
{
|
|
8
|
+
headers: {
|
|
9
|
+
"User-Agent": "Mozilla/5.0"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
res => {
|
|
13
|
+
let data = "";
|
|
14
|
+
|
|
15
|
+
res.on("data", chunk => {
|
|
16
|
+
data += chunk;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
res.on("end", () => {
|
|
20
|
+
resolve(data);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
).on("error", reject);
|
|
24
|
+
});
|
|
25
|
+
}
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
function cleanSentence(sentence) {
|
|
4
|
+
return sentence
|
|
5
|
+
.replace(/\[\d+\]/g, "")
|
|
6
|
+
.replace(/โ/g, "")
|
|
7
|
+
.replace(/US\$\d+[\w,.]*/gi, "")
|
|
8
|
+
.replace(/\$\d+[\w,.]*/g, "")
|
|
9
|
+
.replace(/\s+/g, " ")
|
|
10
|
+
.trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
function removeDuplicates(sentences) {
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
|
|
17
|
+
return sentences.filter(sentence => {
|
|
18
|
+
const key = sentence
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]/g, "")
|
|
21
|
+
.slice(0, 120);
|
|
22
|
+
|
|
23
|
+
if (seen.has(key)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
seen.add(key);
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
function getIntent(query) {
|
|
34
|
+
const q = query.toLowerCase();
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
q.includes("history") ||
|
|
38
|
+
q.includes("founded") ||
|
|
39
|
+
q.includes("created")
|
|
40
|
+
) {
|
|
41
|
+
return "history";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
q.includes("lawsuit") ||
|
|
46
|
+
q.includes("legal") ||
|
|
47
|
+
q.includes("sued")
|
|
48
|
+
) {
|
|
49
|
+
return "legal";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
q.includes("revenue") ||
|
|
54
|
+
q.includes("money") ||
|
|
55
|
+
q.includes("worth")
|
|
56
|
+
) {
|
|
57
|
+
return "business";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return "general";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
function scoreSentence(sentence, keywords, query) {
|
|
65
|
+
|
|
66
|
+
const lower = sentence.toLowerCase();
|
|
67
|
+
|
|
68
|
+
let score = 0;
|
|
69
|
+
|
|
70
|
+
const subject =
|
|
71
|
+
keywords.join(" ");
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// Query matching
|
|
75
|
+
for (const word of keywords) {
|
|
76
|
+
if (lower.includes(word)) {
|
|
77
|
+
score += 3;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
// Exact subject definition
|
|
83
|
+
if (
|
|
84
|
+
lower.startsWith(subject + " is ") ||
|
|
85
|
+
lower.startsWith(subject + " was ")
|
|
86
|
+
) {
|
|
87
|
+
score += 25;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
// Explanatory sentences
|
|
92
|
+
const explanation = [
|
|
93
|
+
" is a ",
|
|
94
|
+
" is an ",
|
|
95
|
+
" allows users ",
|
|
96
|
+
" allows people ",
|
|
97
|
+
" lets users ",
|
|
98
|
+
" provides ",
|
|
99
|
+
" offers ",
|
|
100
|
+
" used to ",
|
|
101
|
+
" designed to "
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const pattern of explanation) {
|
|
105
|
+
if (lower.includes(pattern)) {
|
|
106
|
+
score += 8;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
// History
|
|
112
|
+
if (
|
|
113
|
+
lower.includes("was founded") ||
|
|
114
|
+
lower.includes("was created") ||
|
|
115
|
+
lower.includes("was developed")
|
|
116
|
+
) {
|
|
117
|
+
score += 5;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// Product/features of the main thing
|
|
122
|
+
const relatedProducts = [
|
|
123
|
+
"youtube kids",
|
|
124
|
+
"youtube music",
|
|
125
|
+
"youtube movies",
|
|
126
|
+
"youtube premium",
|
|
127
|
+
"youtube play buttons",
|
|
128
|
+
"youtube tv"
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
for (const product of relatedProducts) {
|
|
132
|
+
if (
|
|
133
|
+
lower.includes(product) &&
|
|
134
|
+
!lower.startsWith(subject)
|
|
135
|
+
) {
|
|
136
|
+
score -= 10;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
// Metadata
|
|
142
|
+
const metadata = [
|
|
143
|
+
"founders",
|
|
144
|
+
"key people",
|
|
145
|
+
"industry",
|
|
146
|
+
"revenue",
|
|
147
|
+
"headquartered",
|
|
148
|
+
"headquarters",
|
|
149
|
+
"employees",
|
|
150
|
+
"ceo",
|
|
151
|
+
"written in",
|
|
152
|
+
"programming language",
|
|
153
|
+
"stock"
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (const word of metadata) {
|
|
157
|
+
if (lower.includes(word)) {
|
|
158
|
+
score -= 15;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
const intent = getIntent(query);
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
// Only punish these if user did not ask
|
|
167
|
+
if (intent !== "legal") {
|
|
168
|
+
if (
|
|
169
|
+
lower.includes("lawsuit") ||
|
|
170
|
+
lower.includes("sued") ||
|
|
171
|
+
lower.includes("court")
|
|
172
|
+
) {
|
|
173
|
+
score -= 20;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if (intent !== "history") {
|
|
179
|
+
if (
|
|
180
|
+
lower.includes("history") ||
|
|
181
|
+
lower.includes("timeline")
|
|
182
|
+
) {
|
|
183
|
+
score -= 10;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if (intent !== "business") {
|
|
189
|
+
if (
|
|
190
|
+
lower.includes("revenue") ||
|
|
191
|
+
lower.includes("profit")
|
|
192
|
+
) {
|
|
193
|
+
score -= 10;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
// Remove opinion/marketing sentences
|
|
199
|
+
const weak = [
|
|
200
|
+
"in conclusion",
|
|
201
|
+
"incredibly popular",
|
|
202
|
+
"act of courage",
|
|
203
|
+
"vision",
|
|
204
|
+
"voice",
|
|
205
|
+
"work with the world"
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
for (const word of weak) {
|
|
209
|
+
if (lower.includes(word)) {
|
|
210
|
+
score -= 15;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
// Length preference
|
|
216
|
+
if (
|
|
217
|
+
sentence.length >= 70 &&
|
|
218
|
+
sentence.length <= 220
|
|
219
|
+
) {
|
|
220
|
+
score += 5;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if (sentence.length > 300) {
|
|
225
|
+
score -= 5;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
return score;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
function createTitle(query) {
|
|
234
|
+
return query
|
|
235
|
+
.replace(/^whats?\s+/i, "")
|
|
236
|
+
.split(" ")
|
|
237
|
+
.map(word =>
|
|
238
|
+
word.charAt(0).toUpperCase() +
|
|
239
|
+
word.slice(1)
|
|
240
|
+
)
|
|
241
|
+
.join(" ");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
export function generate(results, query = "") {
|
|
246
|
+
|
|
247
|
+
const keywords =
|
|
248
|
+
query
|
|
249
|
+
.toLowerCase()
|
|
250
|
+
.replace(/^whats?\s+/i, "")
|
|
251
|
+
.split(/\s+/)
|
|
252
|
+
.filter(Boolean);
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
let sentences = [];
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
for (const result of results) {
|
|
259
|
+
|
|
260
|
+
if (!result.text) continue;
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
const parts =
|
|
264
|
+
result.text
|
|
265
|
+
.split(/[.!?]/)
|
|
266
|
+
.map(cleanSentence)
|
|
267
|
+
.filter(sentence =>
|
|
268
|
+
sentence.length > 40
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
sentences.push(...parts);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
sentences =
|
|
277
|
+
removeDuplicates(sentences);
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
const ranked =
|
|
281
|
+
sentences
|
|
282
|
+
.map(sentence => ({
|
|
283
|
+
sentence,
|
|
284
|
+
score: scoreSentence(
|
|
285
|
+
sentence,
|
|
286
|
+
keywords,
|
|
287
|
+
query
|
|
288
|
+
)
|
|
289
|
+
}))
|
|
290
|
+
.sort(
|
|
291
|
+
(a, b) =>
|
|
292
|
+
b.score - a.score
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
const selected =
|
|
297
|
+
ranked
|
|
298
|
+
.slice(0, 5)
|
|
299
|
+
.map(item => item.sentence);
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
console.log();
|
|
303
|
+
|
|
304
|
+
console.log(
|
|
305
|
+
chalk.bold.cyan(
|
|
306
|
+
createTitle(query)
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
console.log();
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if (!selected.length) {
|
|
314
|
+
console.log(
|
|
315
|
+
chalk.yellow(
|
|
316
|
+
"No useful information found."
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
console.log(
|
|
325
|
+
selected
|
|
326
|
+
.map(sentence =>
|
|
327
|
+
chalk.white("โข " + sentence)
|
|
328
|
+
)
|
|
329
|
+
.join("\n\n")
|
|
330
|
+
);
|
|
331
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { duckSearch } from "./duckduckgo.js";
|
|
4
|
+
import { fetchPage } from "./fetch.js";
|
|
5
|
+
import { extractText } from "./extract.js";
|
|
6
|
+
import { generate } from "./generator.js";
|
|
7
|
+
|
|
8
|
+
const query = process.argv.slice(2).join(" ");
|
|
9
|
+
|
|
10
|
+
if (!query) {
|
|
11
|
+
console.log(`
|
|
12
|
+
Search CLI
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
search <query>
|
|
16
|
+
`);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log("Searching...\n");
|
|
21
|
+
|
|
22
|
+
const results = await duckSearch(query);
|
|
23
|
+
|
|
24
|
+
if (!results.length) {
|
|
25
|
+
console.log("No results found.");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(
|
|
30
|
+
`Found ${results.length} results. Search is Thinking...\n`
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
for (const result of results) {
|
|
34
|
+
try {
|
|
35
|
+
const html = await fetchPage(result.url);
|
|
36
|
+
|
|
37
|
+
result.text = extractText(html);
|
|
38
|
+
|
|
39
|
+
} catch (error) {
|
|
40
|
+
result.text = "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
generate(results, query);
|
package/src/output.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function output(results) {
|
|
2
|
+
|
|
3
|
+
if (!results.length) {
|
|
4
|
+
console.log("No results found.");
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
console.log("\nResults:\n");
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
results.forEach((result, i) => {
|
|
13
|
+
|
|
14
|
+
console.log(
|
|
15
|
+
`${i + 1}. ${result.title}
|
|
16
|
+
${result.url}
|
|
17
|
+
`
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
}
|