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 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(/&amp;/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
+ }
@@ -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
+ }