@marcfargas/skills 0.2.0 → 0.2.1

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Marc
3
+ Copyright (c) 2026 Marc Fargas
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -8,6 +8,7 @@ Reusable skills for AI coding agents. Works with [pi](https://github.com/marioze
8
8
  |----------|-------|-------------|
9
9
  | ☁️ Google Cloud | [gcloud](google-cloud/gcloud/) | GCP CLI with agent safety model — hub + 7 reference files |
10
10
  | 🚀 Release | [pre-release](release/pre-release/) | Pre-release checklist + AI-written changesets via @changesets/cli |
11
+ | 🔍 Search | [web-search](search/web-search/) | Web search + content extraction via [ddgs](https://github.com/deedy5/ddgs) — no API keys |
11
12
  | 🎬 Terminal | [vhs](terminal/vhs/) | Record terminal sessions as GIF/MP4 with [VHS](https://github.com/charmbracelet/vhs) |
12
13
 
13
14
  ## Install
@@ -42,13 +43,26 @@ Copy the skill directory into your agent's skill folder:
42
43
  cp -r google-cloud/gcloud ~/.claude/skills/gcloud
43
44
  ```
44
45
 
45
- ## Skill Design Principles
46
+ ## How We Build Skills
46
47
 
47
- 1. **Safety first** — destructive operations classified and gated, costs flagged
48
- 2. **Hub + spoke** — thin SKILL.md hub (~140 lines) + per-topic reference files loaded on demand
49
- 3. **Agent-native**`--format=json` everywhere, idempotent patterns, error handling
50
- 4. **Portable** — no hardcoded paths or personal config
51
- 5. **Tested** — validated with Gemini, GPT, and Claude before publishing
48
+ ### Multi-Model Review
49
+
50
+ Every skill is reviewed by **3+ models** (Claude, Gemini, GPT) before publishing structure, agent usability, safety, and real-world scenario testing. If an agent can misinterpret an instruction, we find out before you do.
51
+
52
+ ### Safety Classification
53
+
54
+ Every operation is classified: **READ** / **WRITE** / **DESTRUCTIVE** / **EXPENSIVE** / **FORBIDDEN**. Destructive and expensive operations are gated — the agent must confirm before executing, and costs are flagged upfront.
55
+
56
+ ### Progressive Discovery
57
+
58
+ Skills use a **hub + spoke** architecture. The SKILL.md hub is ~140 lines — just enough to match the right skill and know what's available. Detailed per-topic reference files are loaded on demand, keeping your context window lean.
59
+
60
+ ### Also
61
+
62
+ - **Agent-native** — `--format=json` everywhere, idempotent patterns, structured error handling
63
+ - **Portable** — no hardcoded paths, no personal config, works on any machine
64
+ - **Spec-compliant** — validated against the [Agent Skills specification](https://agentskills.io/specification) using [skills-ref](https://github.com/agentskills/agentskills) in CI
65
+ - **Continuous validation** — `agentskills validate` on every push ([validate.yml](.github/workflows/validate.yml)), [pre-release checklist](release/pre-release/) with AI-written changesets, [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers) with provenance
52
66
 
53
67
  ## Structure
54
68
 
@@ -58,19 +72,22 @@ skills/
58
72
  │ └── gcloud/ # 8 files, ~1100 lines total
59
73
  ├── release/
60
74
  │ └── pre-release/ # 1 file
75
+ ├── search/
76
+ │ └── web-search/ # SKILL.md + search.js + content.js
61
77
  ├── terminal/
62
78
  │ └── vhs/ # 1 file
63
79
  └── README.md
64
80
  ```
65
81
 
66
- ## External Skills (planned)
82
+ ## External Skills
67
83
 
68
- Some skills are developed in their own repositories and synced here:
84
+ Some skills live in their own repositories install them directly or via their npm packages:
69
85
 
70
- | Skill | Source Repo | Status |
71
- |-------|-------------|--------|
72
- | odoo | `odoo-toolbox` | Planned |
73
- | go-easy | `go-easy` | Planned |
86
+ | Skill | Description | Install |
87
+ |-------|-------------|---------|
88
+ | [go-easy](https://github.com/marcfargas/go-easy) | Gmail, Drive, Calendar for AI agents — `npx go-gmail`, `npx go-drive`, `npx go-calendar` | `npx skills add marcfargas/go-easy` |
89
+ | [holdpty](https://github.com/marcfargas/holdpty) | Detached PTY sessions — launch, attach, view, record terminal processes | `npx skills add marcfargas/holdpty` |
90
+ | [odoo](https://github.com/marcfargas/odoo-toolbox) | Odoo ERP integration — connect, introspect, automate | `npx skills add marcfargas/odoo-toolbox` |
74
91
 
75
92
  ## Contributing
76
93
 
@@ -101,6 +118,12 @@ agentskills to-prompt path/to/skill-a path/to/skill-b
101
118
 
102
119
  CI runs `agentskills validate` on every push — see [`.github/workflows/validate.yml`](.github/workflows/validate.yml).
103
120
 
121
+ ## Sponsor
122
+
123
+ Building high-quality, multi-model-reviewed agent skills takes serious token budget. If these skills save you time, consider sponsoring:
124
+
125
+ [![GitHub Sponsors](https://img.shields.io/github/sponsors/marcfargas?style=for-the-badge&logo=github&label=Sponsor)](https://github.com/sponsors/marcfargas)
126
+
104
127
  ## License
105
128
 
106
129
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcfargas/skills",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Reusable AI agent skills for pi, Claude Code, Cursor, and any Agent Skills compatible agent",
5
5
  "license": "MIT",
6
6
  "author": "Marc Fargas <marc@marcfargas.com>",
@@ -14,12 +14,15 @@
14
14
  "ai-agent",
15
15
  "skills",
16
16
  "gcloud",
17
- "vhs"
17
+ "pre-release",
18
+ "vhs",
19
+ "web-search"
18
20
  ],
19
21
  "pi": {
20
22
  "skills": [
21
23
  "google-cloud",
22
24
  "release",
25
+ "search",
23
26
  "terminal"
24
27
  ]
25
28
  },
@@ -35,6 +38,7 @@
35
38
  "files": [
36
39
  "google-cloud/",
37
40
  "release/",
41
+ "search/",
38
42
  "terminal/",
39
43
  "README.md",
40
44
  "LICENSE"
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: web-search
3
+ description: >-
4
+ Web search and content extraction using ddgs (multi-engine metasearch). No API keys required.
5
+ Use when: searching documentation, facts, current information, news, fetching web content.
6
+ Triggers: search the web, look up, find information, web search, news search, fetch page.
7
+ ---
8
+
9
+ # Web Search
10
+
11
+ Web search and content extraction using [ddgs](https://github.com/deedy5/ddgs) — a multi-engine metasearch CLI.
12
+ No API keys, no signup, no browser required.
13
+
14
+ ## Setup
15
+
16
+ Install ddgs (run once):
17
+
18
+ ```bash
19
+ uv tool install ddgs
20
+ ```
21
+
22
+ Install Node.js dependencies for content extraction (run once):
23
+
24
+ ```bash
25
+ cd {baseDir}
26
+ npm install
27
+ ```
28
+
29
+ ## Search
30
+
31
+ ```bash
32
+ {baseDir}/search.js "query" # Basic search (5 results)
33
+ {baseDir}/search.js "query" -n 10 # More results
34
+ {baseDir}/search.js "query" --content # Include page content as markdown
35
+ {baseDir}/search.js "query" -t w # Results from last week
36
+ {baseDir}/search.js "query" -t m # Results from last month
37
+ {baseDir}/search.js "query" -r es-es # Results in Spanish
38
+ {baseDir}/search.js "query" -b google # Use Google backend
39
+ {baseDir}/search.js "query" -n 3 --content # Combined options
40
+ ```
41
+
42
+ ### Options
43
+
44
+ - `-n <num>` — Number of results (default: 5)
45
+ - `--content` — Fetch and include page content as markdown
46
+ - `-r <region>` — Region code: `us-en`, `es-es`, `de-de`, `fr-fr`, etc. (default: none)
47
+ - `-t <timelimit>` — Filter by time: `d` (day), `w` (week), `m` (month), `y` (year)
48
+ - `-b <backend>` — Search backend: `auto`, `all`, `bing`, `brave`, `duckduckgo`, `google`, `mojeek`, `yandex`, `yahoo`, `wikipedia` (default: auto)
49
+
50
+ ## News Search
51
+
52
+ ```bash
53
+ {baseDir}/search.js --news "query" # News search
54
+ {baseDir}/search.js --news "query" -n 10 -t w # News from last week
55
+ {baseDir}/search.js --news "query" --content # News with full article content
56
+ ```
57
+
58
+ News backends: `auto`, `all`, `bing`, `duckduckgo`, `yahoo`
59
+
60
+ ## Extract Page Content
61
+
62
+ ```bash
63
+ {baseDir}/content.js https://example.com/article
64
+ ```
65
+
66
+ Fetches a URL and extracts readable content as markdown.
67
+
68
+ ## Output Format
69
+
70
+ ### Text search
71
+
72
+ ```
73
+ --- Result 1 ---
74
+ Title: Page Title
75
+ Link: https://example.com/page
76
+ Snippet: Description from search results
77
+ Content: (if --content flag used)
78
+ Markdown content extracted from the page...
79
+
80
+ --- Result 2 ---
81
+ ...
82
+ ```
83
+
84
+ ### News search
85
+
86
+ ```
87
+ --- Result 1 ---
88
+ Title: Article Title
89
+ Link: https://news.example.com/article
90
+ Date: 2026-02-08T10:18:00+00:00
91
+ Source: Reuters
92
+ Snippet: Article summary...
93
+ Content: (if --content flag used)
94
+ Full article as markdown...
95
+
96
+ --- Result 2 ---
97
+ ...
98
+ ```
99
+
100
+ ## When to Use
101
+
102
+ - Searching for documentation or API references
103
+ - Looking up facts or current information
104
+ - News search for recent events
105
+ - Fetching content from specific URLs
106
+ - Any task requiring web search without interactive browsing
107
+ - When no API key is available (unlike Brave Search)
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Readability } from "@mozilla/readability";
4
+ import { JSDOM } from "jsdom";
5
+ import TurndownService from "turndown";
6
+ import { gfm } from "turndown-plugin-gfm";
7
+
8
+ const url = process.argv[2];
9
+
10
+ if (!url) {
11
+ console.log("Usage: content.js <url>");
12
+ console.log("\nExtracts readable content from a webpage as markdown.");
13
+ console.log("\nExamples:");
14
+ console.log(" content.js https://example.com/article");
15
+ console.log(" content.js https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html");
16
+ process.exit(1);
17
+ }
18
+
19
+ function htmlToMarkdown(html) {
20
+ const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
21
+ turndown.use(gfm);
22
+ turndown.addRule("removeEmptyLinks", {
23
+ filter: (node) => node.nodeName === "A" && !node.textContent?.trim(),
24
+ replacement: () => "",
25
+ });
26
+ return turndown
27
+ .turndown(html)
28
+ .replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "")
29
+ .replace(/ +/g, " ")
30
+ .replace(/\s+,/g, ",")
31
+ .replace(/\s+\./g, ".")
32
+ .replace(/\n{3,}/g, "\n\n")
33
+ .trim();
34
+ }
35
+
36
+ try {
37
+ const response = await fetch(url, {
38
+ headers: {
39
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
40
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
41
+ "Accept-Language": "en-US,en;q=0.9",
42
+ },
43
+ signal: AbortSignal.timeout(15000),
44
+ });
45
+
46
+ if (!response.ok) {
47
+ console.error(`HTTP ${response.status}: ${response.statusText}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ const html = await response.text();
52
+ const dom = new JSDOM(html, { url });
53
+ const reader = new Readability(dom.window.document);
54
+ const article = reader.parse();
55
+
56
+ if (article && article.content) {
57
+ if (article.title) {
58
+ console.log(`# ${article.title}\n`);
59
+ }
60
+ console.log(htmlToMarkdown(article.content));
61
+ process.exit(0);
62
+ }
63
+
64
+ // Fallback: try to extract main content
65
+ const fallbackDoc = new JSDOM(html, { url });
66
+ const body = fallbackDoc.window.document;
67
+ body.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach((el) => el.remove());
68
+
69
+ const title = body.querySelector("title")?.textContent?.trim();
70
+ const main = body.querySelector("main, article, [role='main'], .content, #content") || body.body;
71
+
72
+ if (title) {
73
+ console.log(`# ${title}\n`);
74
+ }
75
+
76
+ const text = main?.innerHTML || "";
77
+ if (text.trim().length > 100) {
78
+ console.log(htmlToMarkdown(text));
79
+ } else {
80
+ console.error("Could not extract readable content from this page.");
81
+ process.exit(1);
82
+ }
83
+ } catch (e) {
84
+ console.error(`Error: ${e.message}`);
85
+ process.exit(1);
86
+ }
@@ -0,0 +1,617 @@
1
+ {
2
+ "name": "web-search",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "web-search",
9
+ "version": "1.0.0",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "@mozilla/readability": "^0.6.0",
13
+ "jsdom": "^27.0.1",
14
+ "turndown": "^7.2.2",
15
+ "turndown-plugin-gfm": "^1.0.2"
16
+ }
17
+ },
18
+ "node_modules/@acemir/cssom": {
19
+ "version": "0.9.31",
20
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
21
+ "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
22
+ "license": "MIT"
23
+ },
24
+ "node_modules/@asamuzakjp/css-color": {
25
+ "version": "4.1.2",
26
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz",
27
+ "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@csstools/css-calc": "^3.0.0",
31
+ "@csstools/css-color-parser": "^4.0.1",
32
+ "@csstools/css-parser-algorithms": "^4.0.0",
33
+ "@csstools/css-tokenizer": "^4.0.0",
34
+ "lru-cache": "^11.2.5"
35
+ }
36
+ },
37
+ "node_modules/@asamuzakjp/dom-selector": {
38
+ "version": "6.7.8",
39
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.8.tgz",
40
+ "integrity": "sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==",
41
+ "license": "MIT",
42
+ "dependencies": {
43
+ "@asamuzakjp/nwsapi": "^2.3.9",
44
+ "bidi-js": "^1.0.3",
45
+ "css-tree": "^3.1.0",
46
+ "is-potential-custom-element-name": "^1.0.1",
47
+ "lru-cache": "^11.2.5"
48
+ }
49
+ },
50
+ "node_modules/@asamuzakjp/nwsapi": {
51
+ "version": "2.3.9",
52
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
53
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
54
+ "license": "MIT"
55
+ },
56
+ "node_modules/@csstools/color-helpers": {
57
+ "version": "6.0.1",
58
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz",
59
+ "integrity": "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==",
60
+ "funding": [
61
+ {
62
+ "type": "github",
63
+ "url": "https://github.com/sponsors/csstools"
64
+ },
65
+ {
66
+ "type": "opencollective",
67
+ "url": "https://opencollective.com/csstools"
68
+ }
69
+ ],
70
+ "license": "MIT-0",
71
+ "engines": {
72
+ "node": ">=20.19.0"
73
+ }
74
+ },
75
+ "node_modules/@csstools/css-calc": {
76
+ "version": "3.0.0",
77
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.0.0.tgz",
78
+ "integrity": "sha512-q4d82GTl8BIlh/dTnVsWmxnbWJeb3kiU8eUH71UxlxnS+WIaALmtzTL8gR15PkYOexMQYVk0CO4qIG93C1IvPA==",
79
+ "funding": [
80
+ {
81
+ "type": "github",
82
+ "url": "https://github.com/sponsors/csstools"
83
+ },
84
+ {
85
+ "type": "opencollective",
86
+ "url": "https://opencollective.com/csstools"
87
+ }
88
+ ],
89
+ "license": "MIT",
90
+ "engines": {
91
+ "node": ">=20.19.0"
92
+ },
93
+ "peerDependencies": {
94
+ "@csstools/css-parser-algorithms": "^4.0.0",
95
+ "@csstools/css-tokenizer": "^4.0.0"
96
+ }
97
+ },
98
+ "node_modules/@csstools/css-color-parser": {
99
+ "version": "4.0.1",
100
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.1.tgz",
101
+ "integrity": "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==",
102
+ "funding": [
103
+ {
104
+ "type": "github",
105
+ "url": "https://github.com/sponsors/csstools"
106
+ },
107
+ {
108
+ "type": "opencollective",
109
+ "url": "https://opencollective.com/csstools"
110
+ }
111
+ ],
112
+ "license": "MIT",
113
+ "dependencies": {
114
+ "@csstools/color-helpers": "^6.0.1",
115
+ "@csstools/css-calc": "^3.0.0"
116
+ },
117
+ "engines": {
118
+ "node": ">=20.19.0"
119
+ },
120
+ "peerDependencies": {
121
+ "@csstools/css-parser-algorithms": "^4.0.0",
122
+ "@csstools/css-tokenizer": "^4.0.0"
123
+ }
124
+ },
125
+ "node_modules/@csstools/css-parser-algorithms": {
126
+ "version": "4.0.0",
127
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
128
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
129
+ "funding": [
130
+ {
131
+ "type": "github",
132
+ "url": "https://github.com/sponsors/csstools"
133
+ },
134
+ {
135
+ "type": "opencollective",
136
+ "url": "https://opencollective.com/csstools"
137
+ }
138
+ ],
139
+ "license": "MIT",
140
+ "peer": true,
141
+ "engines": {
142
+ "node": ">=20.19.0"
143
+ },
144
+ "peerDependencies": {
145
+ "@csstools/css-tokenizer": "^4.0.0"
146
+ }
147
+ },
148
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
149
+ "version": "1.0.26",
150
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz",
151
+ "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==",
152
+ "funding": [
153
+ {
154
+ "type": "github",
155
+ "url": "https://github.com/sponsors/csstools"
156
+ },
157
+ {
158
+ "type": "opencollective",
159
+ "url": "https://opencollective.com/csstools"
160
+ }
161
+ ],
162
+ "license": "MIT-0"
163
+ },
164
+ "node_modules/@csstools/css-tokenizer": {
165
+ "version": "4.0.0",
166
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
167
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
168
+ "funding": [
169
+ {
170
+ "type": "github",
171
+ "url": "https://github.com/sponsors/csstools"
172
+ },
173
+ {
174
+ "type": "opencollective",
175
+ "url": "https://opencollective.com/csstools"
176
+ }
177
+ ],
178
+ "license": "MIT",
179
+ "peer": true,
180
+ "engines": {
181
+ "node": ">=20.19.0"
182
+ }
183
+ },
184
+ "node_modules/@exodus/bytes": {
185
+ "version": "1.12.0",
186
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.12.0.tgz",
187
+ "integrity": "sha512-BuCOHA/EJdPN0qQ5MdgAiJSt9fYDHbghlgrj33gRdy/Yp1/FMCDhU6vJfcKrLC0TPWGSrfH3vYXBQWmFHxlddw==",
188
+ "license": "MIT",
189
+ "engines": {
190
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
191
+ },
192
+ "peerDependencies": {
193
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
194
+ },
195
+ "peerDependenciesMeta": {
196
+ "@noble/hashes": {
197
+ "optional": true
198
+ }
199
+ }
200
+ },
201
+ "node_modules/@mixmark-io/domino": {
202
+ "version": "2.2.0",
203
+ "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
204
+ "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
205
+ "license": "BSD-2-Clause"
206
+ },
207
+ "node_modules/@mozilla/readability": {
208
+ "version": "0.6.0",
209
+ "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
210
+ "integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==",
211
+ "license": "Apache-2.0",
212
+ "engines": {
213
+ "node": ">=14.0.0"
214
+ }
215
+ },
216
+ "node_modules/agent-base": {
217
+ "version": "7.1.4",
218
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
219
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
220
+ "license": "MIT",
221
+ "engines": {
222
+ "node": ">= 14"
223
+ }
224
+ },
225
+ "node_modules/bidi-js": {
226
+ "version": "1.0.3",
227
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
228
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
229
+ "license": "MIT",
230
+ "dependencies": {
231
+ "require-from-string": "^2.0.2"
232
+ }
233
+ },
234
+ "node_modules/css-tree": {
235
+ "version": "3.1.0",
236
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
237
+ "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
238
+ "license": "MIT",
239
+ "dependencies": {
240
+ "mdn-data": "2.12.2",
241
+ "source-map-js": "^1.0.1"
242
+ },
243
+ "engines": {
244
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
245
+ }
246
+ },
247
+ "node_modules/cssstyle": {
248
+ "version": "5.3.7",
249
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz",
250
+ "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==",
251
+ "license": "MIT",
252
+ "dependencies": {
253
+ "@asamuzakjp/css-color": "^4.1.1",
254
+ "@csstools/css-syntax-patches-for-csstree": "^1.0.21",
255
+ "css-tree": "^3.1.0",
256
+ "lru-cache": "^11.2.4"
257
+ },
258
+ "engines": {
259
+ "node": ">=20"
260
+ }
261
+ },
262
+ "node_modules/data-urls": {
263
+ "version": "6.0.1",
264
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz",
265
+ "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==",
266
+ "license": "MIT",
267
+ "dependencies": {
268
+ "whatwg-mimetype": "^5.0.0",
269
+ "whatwg-url": "^15.1.0"
270
+ },
271
+ "engines": {
272
+ "node": ">=20"
273
+ }
274
+ },
275
+ "node_modules/data-urls/node_modules/whatwg-mimetype": {
276
+ "version": "5.0.0",
277
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
278
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
279
+ "license": "MIT",
280
+ "engines": {
281
+ "node": ">=20"
282
+ }
283
+ },
284
+ "node_modules/debug": {
285
+ "version": "4.4.3",
286
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
287
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
288
+ "license": "MIT",
289
+ "dependencies": {
290
+ "ms": "^2.1.3"
291
+ },
292
+ "engines": {
293
+ "node": ">=6.0"
294
+ },
295
+ "peerDependenciesMeta": {
296
+ "supports-color": {
297
+ "optional": true
298
+ }
299
+ }
300
+ },
301
+ "node_modules/decimal.js": {
302
+ "version": "10.6.0",
303
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
304
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
305
+ "license": "MIT"
306
+ },
307
+ "node_modules/entities": {
308
+ "version": "6.0.1",
309
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
310
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
311
+ "license": "BSD-2-Clause",
312
+ "engines": {
313
+ "node": ">=0.12"
314
+ },
315
+ "funding": {
316
+ "url": "https://github.com/fb55/entities?sponsor=1"
317
+ }
318
+ },
319
+ "node_modules/html-encoding-sniffer": {
320
+ "version": "6.0.0",
321
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
322
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
323
+ "license": "MIT",
324
+ "dependencies": {
325
+ "@exodus/bytes": "^1.6.0"
326
+ },
327
+ "engines": {
328
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
329
+ }
330
+ },
331
+ "node_modules/http-proxy-agent": {
332
+ "version": "7.0.2",
333
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
334
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
335
+ "license": "MIT",
336
+ "dependencies": {
337
+ "agent-base": "^7.1.0",
338
+ "debug": "^4.3.4"
339
+ },
340
+ "engines": {
341
+ "node": ">= 14"
342
+ }
343
+ },
344
+ "node_modules/https-proxy-agent": {
345
+ "version": "7.0.6",
346
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
347
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
348
+ "license": "MIT",
349
+ "dependencies": {
350
+ "agent-base": "^7.1.2",
351
+ "debug": "4"
352
+ },
353
+ "engines": {
354
+ "node": ">= 14"
355
+ }
356
+ },
357
+ "node_modules/is-potential-custom-element-name": {
358
+ "version": "1.0.1",
359
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
360
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
361
+ "license": "MIT"
362
+ },
363
+ "node_modules/jsdom": {
364
+ "version": "27.4.0",
365
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
366
+ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
367
+ "license": "MIT",
368
+ "dependencies": {
369
+ "@acemir/cssom": "^0.9.28",
370
+ "@asamuzakjp/dom-selector": "^6.7.6",
371
+ "@exodus/bytes": "^1.6.0",
372
+ "cssstyle": "^5.3.4",
373
+ "data-urls": "^6.0.0",
374
+ "decimal.js": "^10.6.0",
375
+ "html-encoding-sniffer": "^6.0.0",
376
+ "http-proxy-agent": "^7.0.2",
377
+ "https-proxy-agent": "^7.0.6",
378
+ "is-potential-custom-element-name": "^1.0.1",
379
+ "parse5": "^8.0.0",
380
+ "saxes": "^6.0.0",
381
+ "symbol-tree": "^3.2.4",
382
+ "tough-cookie": "^6.0.0",
383
+ "w3c-xmlserializer": "^5.0.0",
384
+ "webidl-conversions": "^8.0.0",
385
+ "whatwg-mimetype": "^4.0.0",
386
+ "whatwg-url": "^15.1.0",
387
+ "ws": "^8.18.3",
388
+ "xml-name-validator": "^5.0.0"
389
+ },
390
+ "engines": {
391
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
392
+ },
393
+ "peerDependencies": {
394
+ "canvas": "^3.0.0"
395
+ },
396
+ "peerDependenciesMeta": {
397
+ "canvas": {
398
+ "optional": true
399
+ }
400
+ }
401
+ },
402
+ "node_modules/lru-cache": {
403
+ "version": "11.2.5",
404
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
405
+ "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==",
406
+ "license": "BlueOak-1.0.0",
407
+ "engines": {
408
+ "node": "20 || >=22"
409
+ }
410
+ },
411
+ "node_modules/mdn-data": {
412
+ "version": "2.12.2",
413
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
414
+ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
415
+ "license": "CC0-1.0"
416
+ },
417
+ "node_modules/ms": {
418
+ "version": "2.1.3",
419
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
420
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
421
+ "license": "MIT"
422
+ },
423
+ "node_modules/parse5": {
424
+ "version": "8.0.0",
425
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
426
+ "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
427
+ "license": "MIT",
428
+ "dependencies": {
429
+ "entities": "^6.0.0"
430
+ },
431
+ "funding": {
432
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
433
+ }
434
+ },
435
+ "node_modules/punycode": {
436
+ "version": "2.3.1",
437
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
438
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
439
+ "license": "MIT",
440
+ "engines": {
441
+ "node": ">=6"
442
+ }
443
+ },
444
+ "node_modules/require-from-string": {
445
+ "version": "2.0.2",
446
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
447
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
448
+ "license": "MIT",
449
+ "engines": {
450
+ "node": ">=0.10.0"
451
+ }
452
+ },
453
+ "node_modules/saxes": {
454
+ "version": "6.0.0",
455
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
456
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
457
+ "license": "ISC",
458
+ "dependencies": {
459
+ "xmlchars": "^2.2.0"
460
+ },
461
+ "engines": {
462
+ "node": ">=v12.22.7"
463
+ }
464
+ },
465
+ "node_modules/source-map-js": {
466
+ "version": "1.2.1",
467
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
468
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
469
+ "license": "BSD-3-Clause",
470
+ "engines": {
471
+ "node": ">=0.10.0"
472
+ }
473
+ },
474
+ "node_modules/symbol-tree": {
475
+ "version": "3.2.4",
476
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
477
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
478
+ "license": "MIT"
479
+ },
480
+ "node_modules/tldts": {
481
+ "version": "7.0.23",
482
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz",
483
+ "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==",
484
+ "license": "MIT",
485
+ "dependencies": {
486
+ "tldts-core": "^7.0.23"
487
+ },
488
+ "bin": {
489
+ "tldts": "bin/cli.js"
490
+ }
491
+ },
492
+ "node_modules/tldts-core": {
493
+ "version": "7.0.23",
494
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz",
495
+ "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==",
496
+ "license": "MIT"
497
+ },
498
+ "node_modules/tough-cookie": {
499
+ "version": "6.0.0",
500
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
501
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
502
+ "license": "BSD-3-Clause",
503
+ "dependencies": {
504
+ "tldts": "^7.0.5"
505
+ },
506
+ "engines": {
507
+ "node": ">=16"
508
+ }
509
+ },
510
+ "node_modules/tr46": {
511
+ "version": "6.0.0",
512
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
513
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
514
+ "license": "MIT",
515
+ "dependencies": {
516
+ "punycode": "^2.3.1"
517
+ },
518
+ "engines": {
519
+ "node": ">=20"
520
+ }
521
+ },
522
+ "node_modules/turndown": {
523
+ "version": "7.2.2",
524
+ "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
525
+ "integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
526
+ "license": "MIT",
527
+ "dependencies": {
528
+ "@mixmark-io/domino": "^2.2.0"
529
+ }
530
+ },
531
+ "node_modules/turndown-plugin-gfm": {
532
+ "version": "1.0.2",
533
+ "resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
534
+ "integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==",
535
+ "license": "MIT"
536
+ },
537
+ "node_modules/w3c-xmlserializer": {
538
+ "version": "5.0.0",
539
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
540
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
541
+ "license": "MIT",
542
+ "dependencies": {
543
+ "xml-name-validator": "^5.0.0"
544
+ },
545
+ "engines": {
546
+ "node": ">=18"
547
+ }
548
+ },
549
+ "node_modules/webidl-conversions": {
550
+ "version": "8.0.1",
551
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
552
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
553
+ "license": "BSD-2-Clause",
554
+ "engines": {
555
+ "node": ">=20"
556
+ }
557
+ },
558
+ "node_modules/whatwg-mimetype": {
559
+ "version": "4.0.0",
560
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
561
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
562
+ "license": "MIT",
563
+ "engines": {
564
+ "node": ">=18"
565
+ }
566
+ },
567
+ "node_modules/whatwg-url": {
568
+ "version": "15.1.0",
569
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
570
+ "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
571
+ "license": "MIT",
572
+ "dependencies": {
573
+ "tr46": "^6.0.0",
574
+ "webidl-conversions": "^8.0.0"
575
+ },
576
+ "engines": {
577
+ "node": ">=20"
578
+ }
579
+ },
580
+ "node_modules/ws": {
581
+ "version": "8.19.0",
582
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
583
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
584
+ "license": "MIT",
585
+ "engines": {
586
+ "node": ">=10.0.0"
587
+ },
588
+ "peerDependencies": {
589
+ "bufferutil": "^4.0.1",
590
+ "utf-8-validate": ">=5.0.2"
591
+ },
592
+ "peerDependenciesMeta": {
593
+ "bufferutil": {
594
+ "optional": true
595
+ },
596
+ "utf-8-validate": {
597
+ "optional": true
598
+ }
599
+ }
600
+ },
601
+ "node_modules/xml-name-validator": {
602
+ "version": "5.0.0",
603
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
604
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
605
+ "license": "Apache-2.0",
606
+ "engines": {
607
+ "node": ">=18"
608
+ }
609
+ },
610
+ "node_modules/xmlchars": {
611
+ "version": "2.2.0",
612
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
613
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
614
+ "license": "MIT"
615
+ }
616
+ }
617
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "web-search",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Web search via ddgs with optional content extraction",
6
+ "license": "MIT",
7
+ "dependencies": {
8
+ "@mozilla/readability": "^0.6.0",
9
+ "jsdom": "^27.0.1",
10
+ "turndown": "^7.2.2",
11
+ "turndown-plugin-gfm": "^1.0.2"
12
+ }
13
+ }
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from "child_process";
4
+ import { readFileSync, unlinkSync } from "fs";
5
+ import { join } from "path";
6
+ import { tmpdir } from "os";
7
+ import { randomBytes } from "crypto";
8
+ import { Readability } from "@mozilla/readability";
9
+ import { JSDOM } from "jsdom";
10
+ import TurndownService from "turndown";
11
+ import { gfm } from "turndown-plugin-gfm";
12
+
13
+ const args = process.argv.slice(2);
14
+
15
+ // Parse --content flag
16
+ const contentIndex = args.indexOf("--content");
17
+ const fetchContent = contentIndex !== -1;
18
+ if (fetchContent) args.splice(contentIndex, 1);
19
+
20
+ // Parse --news flag
21
+ const newsIndex = args.indexOf("--news");
22
+ const isNews = newsIndex !== -1;
23
+ if (isNews) args.splice(newsIndex, 1);
24
+
25
+ // Parse -n <num>
26
+ let numResults = 5;
27
+ const nIndex = args.indexOf("-n");
28
+ if (nIndex !== -1 && args[nIndex + 1]) {
29
+ numResults = parseInt(args[nIndex + 1], 10);
30
+ args.splice(nIndex, 2);
31
+ }
32
+
33
+ // Parse -r <region>
34
+ let region = null;
35
+ const rIndex = args.indexOf("-r");
36
+ if (rIndex !== -1 && args[rIndex + 1]) {
37
+ region = args[rIndex + 1];
38
+ args.splice(rIndex, 2);
39
+ }
40
+
41
+ // Parse -t <timelimit>
42
+ let timelimit = null;
43
+ const tIndex = args.indexOf("-t");
44
+ if (tIndex !== -1 && args[tIndex + 1]) {
45
+ timelimit = args[tIndex + 1];
46
+ args.splice(tIndex, 2);
47
+ }
48
+
49
+ // Parse -b <backend>
50
+ let backend = null;
51
+ const bIndex = args.indexOf("-b");
52
+ if (bIndex !== -1 && args[bIndex + 1]) {
53
+ backend = args[bIndex + 1];
54
+ args.splice(bIndex, 2);
55
+ }
56
+
57
+ const query = args.join(" ");
58
+
59
+ if (!query) {
60
+ console.log("Usage: search.js [--news] <query> [-n <num>] [--content] [-r <region>] [-t <timelimit>] [-b <backend>]");
61
+ console.log("\nOptions:");
62
+ console.log(" --news News search instead of web search");
63
+ console.log(" -n <num> Number of results (default: 5)");
64
+ console.log(" --content Fetch readable content as markdown");
65
+ console.log(" -r <region> Region: us-en, es-es, de-de, fr-fr, etc.");
66
+ console.log(" -t <timelimit> Time filter: d (day), w (week), m (month), y (year)");
67
+ console.log(" -b <backend> Backend: auto, all, bing, brave, duckduckgo, google, etc.");
68
+ console.log("\nExamples:");
69
+ console.log(' search.js "javascript async await"');
70
+ console.log(' search.js "rust programming" -n 10');
71
+ console.log(' search.js "climate change" --content');
72
+ console.log(' search.js --news "AI agents" -t w');
73
+ process.exit(1);
74
+ }
75
+
76
+ function htmlToMarkdown(html) {
77
+ const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
78
+ turndown.use(gfm);
79
+ turndown.addRule("removeEmptyLinks", {
80
+ filter: (node) => node.nodeName === "A" && !node.textContent?.trim(),
81
+ replacement: () => "",
82
+ });
83
+ return turndown
84
+ .turndown(html)
85
+ .replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "")
86
+ .replace(/ +/g, " ")
87
+ .replace(/\s+,/g, ",")
88
+ .replace(/\s+\./g, ".")
89
+ .replace(/\n{3,}/g, "\n\n")
90
+ .trim();
91
+ }
92
+
93
+ async function fetchPageContent(url) {
94
+ try {
95
+ const response = await fetch(url, {
96
+ headers: {
97
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
98
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
99
+ },
100
+ signal: AbortSignal.timeout(10000),
101
+ });
102
+
103
+ if (!response.ok) {
104
+ return `(HTTP ${response.status})`;
105
+ }
106
+
107
+ const html = await response.text();
108
+ const dom = new JSDOM(html, { url });
109
+ const reader = new Readability(dom.window.document);
110
+ const article = reader.parse();
111
+
112
+ if (article && article.content) {
113
+ return htmlToMarkdown(article.content).substring(0, 5000);
114
+ }
115
+
116
+ // Fallback: try to get main content
117
+ const fallbackDoc = new JSDOM(html, { url });
118
+ const body = fallbackDoc.window.document;
119
+ body.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach((el) => el.remove());
120
+ const main = body.querySelector("main, article, [role='main'], .content, #content") || body.body;
121
+ const text = main?.textContent || "";
122
+
123
+ if (text.trim().length > 100) {
124
+ return text.trim().substring(0, 5000);
125
+ }
126
+
127
+ return "(Could not extract content)";
128
+ } catch (e) {
129
+ return `(Error: ${e.message})`;
130
+ }
131
+ }
132
+
133
+ function runDdgs(query, numResults, isNews, region, timelimit, backend) {
134
+ const tmpFile = join(tmpdir(), `ddgs-${randomBytes(6).toString("hex")}.json`);
135
+ const subcommand = isNews ? "news" : "text";
136
+
137
+ const ddgsArgs = [subcommand, "-q", query, "-m", String(numResults), "-o", tmpFile];
138
+
139
+ if (region) {
140
+ ddgsArgs.push("-r", region);
141
+ }
142
+ if (timelimit) {
143
+ ddgsArgs.push("-t", timelimit);
144
+ }
145
+ if (backend) {
146
+ ddgsArgs.push("-b", backend);
147
+ }
148
+
149
+ try {
150
+ execFileSync("ddgs", ddgsArgs, {
151
+ stdio: ["pipe", "pipe", "pipe"],
152
+ timeout: 30000,
153
+ });
154
+ } catch (e) {
155
+ // ddgs may write output to file even if it exits with error
156
+ // (e.g. "Aborted!" after printing results)
157
+ }
158
+
159
+ try {
160
+ const raw = readFileSync(tmpFile, "utf-8");
161
+ unlinkSync(tmpFile);
162
+ return JSON.parse(raw);
163
+ } catch {
164
+ try {
165
+ unlinkSync(tmpFile);
166
+ } catch {}
167
+ throw new Error("ddgs produced no results. Is ddgs installed? Run: uv tool install ddgs");
168
+ }
169
+ }
170
+
171
+ // Main
172
+ try {
173
+ const rawResults = runDdgs(query, numResults, isNews, region, timelimit, backend);
174
+
175
+ if (!rawResults || rawResults.length === 0) {
176
+ console.error("No results found.");
177
+ process.exit(0);
178
+ }
179
+
180
+ // Normalize results to a common shape
181
+ const results = rawResults.map((r) => ({
182
+ title: r.title || "",
183
+ link: r.href || r.url || "",
184
+ snippet: r.body || "",
185
+ date: r.date || "",
186
+ source: r.source || "",
187
+ }));
188
+
189
+ if (fetchContent) {
190
+ for (const result of results) {
191
+ result.content = await fetchPageContent(result.link);
192
+ }
193
+ }
194
+
195
+ for (let i = 0; i < results.length; i++) {
196
+ const r = results[i];
197
+ console.log(`--- Result ${i + 1} ---`);
198
+ console.log(`Title: ${r.title}`);
199
+ console.log(`Link: ${r.link}`);
200
+ if (r.date) {
201
+ console.log(`Date: ${r.date}`);
202
+ }
203
+ if (r.source) {
204
+ console.log(`Source: ${r.source}`);
205
+ }
206
+ console.log(`Snippet: ${r.snippet}`);
207
+ if (r.content) {
208
+ console.log(`Content:\n${r.content}`);
209
+ }
210
+ console.log("");
211
+ }
212
+ } catch (e) {
213
+ console.error(`Error: ${e.message}`);
214
+ process.exit(1);
215
+ }