@ryukin-dev/pi-featherless-kali 1.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0
4
+
5
+ - Package auf npm unter `@ryukin-dev/pi-featherless-kali` veröffentlicht.
6
+ - KaliAI CLI-Binary `kali-ai` hinzugefügt:
7
+ - Menu statt direktem `pi` Start.
8
+ - `kali-ai chat` startet die Chat UI.
9
+ - `kali-ai update` führt das Update durch.
10
+ - `kali-ai whatsnew` zeigt die neuesten Änderungen.
11
+ - Extensions und Skills werden automatisch in `~/.pi/agent/` eingerichtet.
12
+ - Update-Verfügbarkeitsbanner oben in der Chat UI.
13
+ - Neue Chat-Befehle: `/kali-update`, `/kali-whatsnew`.
14
+ - Package-Scope auf `@earendil-works/pi-*` umgestellt.
15
+ - Featherless-Provider ist jetzt API-Key-basiert (`/login` → API keys).
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # pi-featherless — Kali Linux Rebuild
2
+
3
+ > **Attribution:** This is a rebuilt, Kali Linux-targeted copy of the original [`CodeDoes/pi-featherless`](https://github.com/CodeDoes/pi-featherless) project. The original code, structure, and model registry were created by **CodeDoes** / **KitAstro** (AstralCosmo). This repository fixes the upstream syntax error, restores the build, adapts the package scope for the published Pi agent, and adds Kali install/update paths.
4
+
5
+ ## What was changed compared to upstream?
6
+
7
+ | Item | Upstream | This rebuild |
8
+ |------|----------|--------------|
9
+ | `src/models.ts` | Dangling `tool_use: true,` block (syntax error) | Fixed — orphan block removed |
10
+ | `src/models.ts` `getModelClass()` | Only exact-id lookup | Falls back to known class patterns so removed-but-still-referenced models behave correctly |
11
+ | `package.json` | Self-dependency `@codedoes/pi-featherless`: `"."` | Removed — avoids pnpm registry 404 |
12
+ | Package scope | `@mariozechner/pi-*` | Switched to `@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent` |
13
+ | Distribution | Nur Source-Clone | npm-Paket `@earendil-works/pi-featherless-kali` mit `kali-ai` CLI |
14
+ | `src/handlers/provider.ts` | OAuth-style login | API-key provider using `$FEATHERLESS_API_KEY`; appears under `/login` -> **API keys** |
15
+ | Type-check command | Missing | `pnpm tsc --noEmit` now works |
16
+ | Install script | None | `install-kali.sh` for global source install |
17
+ | Update script | None | `update-kali.sh` pulls latest changes, reinstalls, and re-runs tests |
18
+ | Bundled skills | None | `websearch` and `kali-admin` |
19
+ | Update-Banner | None | Automatische "Update verfügbar"-Meldung oben in der Chat UI |
20
+ | Code style | File-level JSDoc and section-divider comments | Removed to avoid AI-typical comment patterns |
21
+
22
+ All functional source logic is preserved 1:1 from the original repository; only imports, provider registration, build/install metadata, CLI helpers, and comments were changed.
23
+
24
+ ## Requirements
25
+
26
+ - Kali Linux (native or WSL)
27
+ - Node.js ≥ 20 (so `corepack` is available)
28
+ `sudo apt update && sudo apt install -y nodejs npm`
29
+ - `pnpm` (enabled via corepack)
30
+ - A Featherless AI API key: https://featherless.ai/account/api-keys
31
+
32
+ ## Install via npm
33
+
34
+ ```bash
35
+ npm install -g @earendil-works/pi-coding-agent
36
+ npm install -g @ryukin-dev/pi-featherless-kali
37
+ ```
38
+
39
+ Danach reicht im Terminal:
40
+
41
+ ```bash
42
+ kaliai
43
+ ```
44
+
45
+ `kaliai` startet die Chat UI. `kaliai Update` aktualisiert das Paket.
46
+
47
+ ### Direktbefehle
48
+
49
+ ```bash
50
+ kaliai # Chat UI starten
51
+ kaliai Update # KaliAI aktualisieren
52
+ kaliai whatsnew # Neueste Änderungen anzeigen
53
+ ```
54
+
55
+ > Der Scope ist `@ryukin-dev`, weil `@earendil-works` eine fremde npm-Organisation ist.
56
+
57
+ ## Install aus dem Source-Repo
58
+
59
+ ```bash
60
+ chmod +x install-kali.sh
61
+ ./install-kali.sh # global install to ~/.pi/agent/extensions/pi-featherless
62
+ ```
63
+
64
+ Project-local install:
65
+
66
+ ```bash
67
+ ./install-kali.sh --project /path/to/your/project
68
+ ```
69
+
70
+ ## Update eines Source-Installs
71
+
72
+ ```bash
73
+ chmod +x update-kali.sh
74
+ ./update-kali.sh
75
+ ```
76
+
77
+ Project-local update:
78
+
79
+ ```bash
80
+ ./update-kali.sh --project /path/to/your/project
81
+ ```
82
+
83
+ Skip verification with `--skip-tests`; discard local edits with `--force`.
84
+
85
+ ## Verify
86
+
87
+ ```bash
88
+ cd ~/.pi/agent/extensions/pi-featherless
89
+ pnpm test # vitest — 13 tests should pass
90
+ pnpm tsc --noEmit
91
+ ```
92
+
93
+ ## Quick API test
94
+
95
+ ```bash
96
+ export FEATHERLESS_API_KEY="sk-..."
97
+ pnpm exec tsx test-api.ts ping
98
+ ```
99
+
100
+ ## Usage in the Pi Chat UI
101
+
102
+ Because the provider is configured as an API-key provider, it appears under **API keys** in the login menu.
103
+
104
+ ```text
105
+ /login
106
+ # Choose API keys -> Featherless AI, then paste your key.
107
+
108
+ /model zai-org/GLM-5
109
+
110
+ # Later, clear the stored key with:
111
+ /logout
112
+ ```
113
+
114
+ ### KaliAI-Befehle in der Chat UI
115
+
116
+ ```text
117
+ /update # Auf neueste npm-Version aktualisieren
118
+ /whatsnew # Changelog anzeigen
119
+ ```
120
+
121
+ Wenn ein Update verfügbar ist, erscheint oben in der Chat UI ein Banner mit der neuen Version. Nach dem Update wird das Changelog kurz eingeblendet, bis das Banner verschwindet.
122
+
123
+ ## Bundled skills
124
+
125
+ - **websearch** — search the web via Brave (with `BRAVE_API_KEY`) or DuckDuckGo, and extract readable article text from URLs.
126
+ - **kali-admin** — authorizes the agent to run shell commands, use `sudo`, install packages with `apt`, manage services, and use Kali's security tooling.
127
+
128
+ ## Troubleshooting
129
+
130
+ - `pnpm: command not found` → `corepack enable && corepack prepare pnpm@latest --activate`
131
+ - `ERR_PNPM_FETCH_404 @codedoes/pi-featherless` → Use this rebuilt copy (self-dependency removed)
132
+ - TypeScript missing → `pnpm install` (typescript/tsx are devDependencies)
133
+ - Cannot find module `@earendil-works/pi-coding-agent` → Install the main agent globally: `npm install -g @earendil-works/pi-coding-agent`
134
+
135
+ **Original project:** [CodeDoes/pi-featherless](https://github.com/CodeDoes/pi-featherless)
136
+ **Rebuild author:** Alessandro / RyuKin-Dev
package/bin/kaliai.js ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync, execFileSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, rmSync, symlinkSync, cpSync, readFileSync, readdirSync } from "node:fs";
4
+ import { homedir, platform } from "node:os";
5
+ import { dirname, join, basename } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const SOURCE_DIR = join(__dirname, "..");
10
+ const HOME = homedir();
11
+ const AGENT_DIR = join(HOME, ".pi", "agent");
12
+ const EXT_DIR = join(AGENT_DIR, "extensions", "pi-featherless");
13
+ const SKILLS_DIR = join(AGENT_DIR, "skills");
14
+ const PI_PACKAGE = "@earendil-works/pi-coding-agent";
15
+ const NPM_PACKAGE = "@ryukin-dev/pi-featherless-kali";
16
+
17
+ function print(...args) {
18
+ console.log(...args);
19
+ }
20
+
21
+ function getGlobalNpmRoot() {
22
+ try {
23
+ return execFileSync("npm", ["root", "-g"], { encoding: "utf8" }).trim();
24
+ } catch {
25
+ return undefined;
26
+ }
27
+ }
28
+
29
+ function getPiCliPath() {
30
+ const root = getGlobalNpmRoot();
31
+ if (!root) return undefined;
32
+ return join(root, PI_PACKAGE, "dist", "cli.js");
33
+ }
34
+
35
+ function readPackageVersion(dir) {
36
+ try {
37
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
38
+ return pkg.version;
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ function readChangelog(dir) {
45
+ try {
46
+ const text = readFileSync(join(dir, "CHANGELOG.md"), "utf8");
47
+ const lines = text.split("\n");
48
+ const start = lines.findIndex((l) => l.startsWith("## "));
49
+ if (start === -1) return "Kein Changelog vorhanden.";
50
+ const end = lines.findIndex((l, i) => i > start && l.startsWith("## "));
51
+ return lines
52
+ .slice(start, end === -1 ? undefined : end)
53
+ .join("\n")
54
+ .trim();
55
+ } catch {
56
+ return "Changelog nicht lesbar.";
57
+ }
58
+ }
59
+
60
+ function installSkillDeps(skillPath) {
61
+ if (existsSync(join(skillPath, "package.json")) && !existsSync(join(skillPath, "node_modules"))) {
62
+ print(` Installiere Skill-Abhängigkeiten: ${basename(skillPath)}`);
63
+ spawnSync("npm", ["install"], { cwd: skillPath, stdio: "inherit" });
64
+ }
65
+ }
66
+
67
+ function linkOrCopy(source, target) {
68
+ if (existsSync(target)) rmSync(target, { recursive: true, force: true });
69
+ try {
70
+ symlinkSync(source, target, platform() === "win32" ? "junction" : "dir");
71
+ } catch {
72
+ cpSync(source, target, { recursive: true });
73
+ }
74
+ }
75
+
76
+ function installExtension() {
77
+ print("==> KaliAI Extension wird eingerichtet...");
78
+ if (existsSync(EXT_DIR)) rmSync(EXT_DIR, { recursive: true, force: true });
79
+ cpSync(SOURCE_DIR, EXT_DIR, {
80
+ recursive: true,
81
+ filter: (src) =>
82
+ !src.includes("node_modules") &&
83
+ !src.includes(".git"),
84
+ });
85
+
86
+ const skillsSource = join(SOURCE_DIR, "skills");
87
+ if (existsSync(skillsSource)) {
88
+ mkdirSync(SKILLS_DIR, { recursive: true });
89
+ for (const name of readdirSync(skillsSource)) {
90
+ const src = join(skillsSource, name);
91
+ const dst = join(SKILLS_DIR, name);
92
+ linkOrCopy(src, dst);
93
+ installSkillDeps(dst);
94
+ }
95
+ }
96
+ print("==> KaliAI Extension bereit.");
97
+ }
98
+
99
+ async function startChat() {
100
+ installExtension();
101
+ const piCli = getPiCliPath();
102
+ if (!piCli || !existsSync(piCli)) {
103
+ print("Fehler: Pi Coding Agent nicht gefunden.");
104
+ print(`Installiere ihn mit:`);
105
+ print(` npm install -g ${PI_PACKAGE}`);
106
+ process.exit(1);
107
+ }
108
+ print("==> Starte KaliAI Chat UI...");
109
+ spawnSync(process.execPath, [piCli], { stdio: "inherit" });
110
+ }
111
+
112
+ async function runUpdate() {
113
+ print("==> KaliAI Update wird durchgeführt...");
114
+ const before = readPackageVersion(SOURCE_DIR);
115
+ const result = spawnSync("npm", ["install", "-g", `${NPM_PACKAGE}@latest`], {
116
+ stdio: "inherit",
117
+ });
118
+ if (result.status !== 0) {
119
+ print("Update fehlgeschlagen.");
120
+ process.exit(result.status ?? 1);
121
+ }
122
+ installExtension();
123
+ const after = readPackageVersion(SOURCE_DIR);
124
+ print("==> KaliAI aktualisiert" + (before && after ? `: v${before} -> v${after}` : ""));
125
+ print("");
126
+ print(readChangelog(SOURCE_DIR));
127
+ print("");
128
+ print("Starte KaliAI neu, um die Änderungen zu laden.");
129
+ }
130
+
131
+ function showWhatsNew() {
132
+ print("==> KaliAI Changelog");
133
+ print("");
134
+ print(readChangelog(SOURCE_DIR));
135
+ }
136
+
137
+ const argv = process.argv.slice(2);
138
+ const first = argv[0] ?? "";
139
+
140
+ if (first.toLowerCase() === "update") {
141
+ runUpdate();
142
+ } else if (first.toLowerCase() === "whatsnew" || first.toLowerCase() === "changelog") {
143
+ showWhatsNew();
144
+ } else {
145
+ startChat();
146
+ }
@@ -0,0 +1,16 @@
1
+
2
+
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import { registerProvider } from "../src/handlers/provider";
5
+ import { registerConcurrencyTracking } from "../src/handlers/concurrency";
6
+ import { registerContextTracking } from "../src/handlers/context";
7
+ import { registerCompaction } from "../src/handlers/compaction";
8
+ import { registerUpdateCheck } from "../src/handlers/update-check";
9
+
10
+ export default function (pi: ExtensionAPI) {
11
+ registerProvider(pi);
12
+ registerConcurrencyTracking(pi);
13
+ registerContextTracking(pi);
14
+ registerCompaction(pi);
15
+ registerUpdateCheck(pi);
16
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@ryukin-dev/pi-featherless-kali",
3
+ "version": "1.1.0",
4
+ "description": "Featherless Provider for Pi with accurate tokenization and concurrency tracking.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "kaliai": "bin/kaliai.js",
9
+ "KaliAi": "bin/kaliai.js"
10
+ },
11
+ "files": [
12
+ "extensions/",
13
+ "src/",
14
+ "skills/",
15
+ "bin/",
16
+ "CHANGELOG.md",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "test": "vitest run",
24
+ "publish-package": "pnpm publish --access public --no-git-checks",
25
+ "test:watch": "vitest",
26
+ "test:ui": "vitest --ui",
27
+ "bench": "npx tsx bench/run-terminal-bench.ts",
28
+ "bench:fast": "npx tsx bench/run-terminal-bench.ts --fast",
29
+ "bench:quick": "npx tsx bench/run-terminal-bench.ts --quick",
30
+ "bench:full": "npx tsx bench/run-terminal-bench.ts"
31
+ },
32
+ "keywords": [
33
+ "pi",
34
+ "pi-package",
35
+ "coding-agent",
36
+ "extension",
37
+ "provider",
38
+ "featherless",
39
+ "llm"
40
+ ],
41
+ "author": "",
42
+ "license": "ISC",
43
+ "packageManager": "pnpm@10.33.0",
44
+ "peerDependencies": {
45
+ "@earendil-works/pi-ai": "*",
46
+ "@earendil-works/pi-coding-agent": "*",
47
+ "@sinclair/typebox": "*"
48
+ },
49
+ "pi": {
50
+ "extensions": [
51
+ "extensions/**/*.ts"
52
+ ],
53
+ "skills": [
54
+ "skills/**/SKILL.md"
55
+ ]
56
+ },
57
+ "dependencies": {
58
+ "JSONStream": "^1.3.5",
59
+ "openai": "^6.33.0",
60
+ "puppeteer": "^24.40.0"
61
+ },
62
+ "devDependencies": {
63
+ "@earendil-works/pi-ai": "^0.80.2",
64
+ "@earendil-works/pi-coding-agent": "^0.80.2",
65
+ "@vitest/ui": "^4.1.3",
66
+ "dotenv": "^17.4.1",
67
+ "tsx": "^4.19.0",
68
+ "typescript": "^5.8.0",
69
+ "vitest": "^4.1.3"
70
+ }
71
+ }
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: kali-admin
3
+ description: Authorize the agent to act as a Kali Linux system administrator. Use for package installation, root commands, system configuration, service management, network tooling, and general shell tasks.
4
+ ---
5
+
6
+ # Kali Administrator
7
+
8
+ You are operating on Kali Linux. You may use the full set of system tools available in the shell. You have access to `sudo` and can run root-level commands when the user asks for system changes.
9
+
10
+ ## Allowed operations
11
+
12
+ - Run normal shell commands, scripts, and pipelines.
13
+ - Use `sudo` whenever a command requires root privileges.
14
+ - Install, update, or remove packages with `apt`.
15
+ - Start, stop, enable, or disable services with `systemctl`.
16
+ - Edit system configuration files in `/etc` or elsewhere.
17
+ - Run security/network tools such as `nmap`, `masscan`, `wireshark-cli`, `metasploit`, etc.
18
+ - Read logs from `/var/log` or via `journalctl`.
19
+
20
+ ## Safety rules
21
+
22
+ - Ask the user for explicit confirmation before destructive actions.
23
+ - Destructive actions include deleting data, formatting drives, wiping logs, modifying critical boot configuration, running exploits against live targets, or mass network scanning without scope.
24
+ - Never expose or write secrets, passwords, API keys, or private keys to disk.
25
+ - When editing system files, prefer to create a backup when practical.
26
+ - If a command fails due to missing privileges, retry with `sudo` rather than giving up.
27
+
28
+ ## Tools
29
+
30
+ Use the built-in shell tool for commands and the read/edit/write tools for files. Combine tools when needed.
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: websearch
3
+ description: Web search and content extraction. Uses Brave Search API if BRAVE_API_KEY is set, otherwise falls back to DuckDuckGo scraping. Can extract readable article text from any URL via Mozilla Readability. Use when the user asks for current web information, documentation lookup, news, facts, or URL content extraction.
4
+ ---
5
+
6
+ # Websearch
7
+
8
+ ## Setup
9
+
10
+ Run once to install dependencies:
11
+
12
+ ```bash
13
+ cd ~/.pi/agent/skills/websearch && npm install
14
+ ```
15
+
16
+ ## Usage: Search
17
+
18
+ ```bash
19
+ # DuckDuckGo fallback (default if no BRAVE_API_KEY)
20
+ node search.js "typescript decorator pattern"
21
+
22
+ # Brave API (set BRAVE_API_KEY env var)
23
+ node search.js "latest react server components" --limit 5
24
+
25
+ # JSON output for piping
26
+ node search.js "openai codex" --json --limit 3
27
+ ```
28
+
29
+ ## Usage: Content Extraction
30
+
31
+ ```bash
32
+ # Extract readable article text from any URL
33
+ node extract.js "https://example.com/blog-post"
34
+
35
+ # JSON output
36
+ node extract.js "https://example.com" --json
37
+ ```
38
+
39
+ ## Notes
40
+
41
+ - Respect robots.txt and rate limits.
42
+ - If DuckDuckGo blocks requests, set a BRAVE_API_KEY environment variable.
43
+ - extract.js strips ads, navigation and boilerplate to return clean article text.
@@ -0,0 +1,65 @@
1
+ import { JSDOM } from 'jsdom';
2
+ import { Readability } from '@mozilla/readability';
3
+
4
+ const url = process.argv[2];
5
+ const isJson = process.argv.includes('--json');
6
+
7
+ if (!url) {
8
+ console.error('Usage: node extract.js <url> [--json]');
9
+ process.exit(1);
10
+ }
11
+
12
+ async function main() {
13
+ try {
14
+ const resp = await fetch(url, {
15
+ headers: {
16
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0',
17
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
18
+ },
19
+ });
20
+
21
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
22
+
23
+ const contentType = resp.headers.get('content-type') || '';
24
+ if (!contentType.includes('text/html')) {
25
+ throw new Error(`Unsupported content type: ${contentType}`);
26
+ }
27
+
28
+ const html = await resp.text();
29
+ const dom = new JSDOM(html, { url });
30
+ const reader = new Readability(dom.window.document);
31
+ const article = reader.parse();
32
+
33
+ if (!article) {
34
+ throw new Error('Could not extract readable content from this URL');
35
+ }
36
+
37
+ if (isJson) {
38
+ console.log(
39
+ JSON.stringify(
40
+ {
41
+ title: article.title || '',
42
+ byline: article.byline || '',
43
+ excerpt: article.excerpt || '',
44
+ length: article.length || 0,
45
+ textContent: article.textContent || '',
46
+ siteName: article.siteName || '',
47
+ },
48
+ null,
49
+ 2
50
+ )
51
+ );
52
+ } else {
53
+ console.log(`# ${article.title || 'Untitled'}`);
54
+ if (article.byline) console.log(`By: ${article.byline}`);
55
+ if (article.excerpt) console.log(`> ${article.excerpt}`);
56
+ console.log('');
57
+ console.log(article.textContent || '');
58
+ }
59
+ } catch (err) {
60
+ console.error('Error:', err.message);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ main();
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "pi-websearch-skill",
3
+ "version": "1.0.0",
4
+ "description": "Web search and content extraction skill for pi",
5
+ "type": "module",
6
+ "private": true,
7
+ "scripts": {
8
+ "search": "node search.js",
9
+ "extract": "node extract.js"
10
+ },
11
+ "dependencies": {
12
+ "cheerio": "^1.0.0-rc.12",
13
+ "jsdom": "^24.1.0",
14
+ "@mozilla/readability": "^0.5.0"
15
+ }
16
+ }
@@ -0,0 +1,110 @@
1
+ import { env } from 'node:process';
2
+
3
+ const query = process.argv.slice(2).find((a) => !a.startsWith('--')) || '';
4
+ const isJson = process.argv.includes('--json');
5
+ const rawLimit = process.argv[process.argv.indexOf('--limit') + 1];
6
+ const limit = Number.isFinite(Number(rawLimit)) ? Number(rawLimit) : 10;
7
+
8
+ async function braveSearch(q) {
9
+ const key = env.BRAVE_API_KEY;
10
+ if (!key) throw new Error('BRAVE_API_KEY not set');
11
+
12
+ const url = new URL('https://api.search.brave.com/res/v1/web/search');
13
+ url.searchParams.set('q', q);
14
+ url.searchParams.set('count', String(limit));
15
+ url.searchParams.set('offset', '0');
16
+
17
+ const resp = await fetch(url, {
18
+ headers: {
19
+ 'X-Subscription-Token': key,
20
+ 'Accept': 'application/json',
21
+ },
22
+ });
23
+
24
+ if (!resp.ok) {
25
+ const text = await resp.text().catch(() => '');
26
+ throw new Error(`Brave API ${resp.status}: ${text}`);
27
+ }
28
+
29
+ const data = await resp.json();
30
+ return (data.web?.results || []).map((r) => ({
31
+ title: r.title || '',
32
+ url: r.url || '',
33
+ description: r.description || '',
34
+ source: 'brave',
35
+ }));
36
+ }
37
+
38
+ async function ddgSearch(q) {
39
+ const body = new URLSearchParams({ q: q, kl: 'en-us' });
40
+
41
+ const resp = await fetch('https://html.duckduckgo.com/html/', {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/x-www-form-urlencoded',
45
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.0',
46
+ },
47
+ body: body.toString(),
48
+ });
49
+
50
+ if (!resp.ok) throw new Error(`DuckDuckGo returned ${resp.status}`);
51
+
52
+ const html = await resp.text();
53
+ const { load } = await import('cheerio');
54
+ const $ = load(html);
55
+ const results = [];
56
+
57
+ $('.result').each((_i, el) => {
58
+ if (results.length >= limit) return false;
59
+ const title = $(el).find('.result__a').text().trim();
60
+ let url = $(el).find('.result__a').attr('href');
61
+ const description = $(el).find('.result__snippet').text().trim();
62
+
63
+ if (!title || !url) return;
64
+ if (url.startsWith('//')) url = 'https:' + url;
65
+ if (url.startsWith('/')) url = 'https://duckduckgo.com' + url;
66
+
67
+ results.push({ title, url, description, source: 'ddg' });
68
+ });
69
+
70
+ return results;
71
+ }
72
+
73
+ async function main() {
74
+ if (!query) {
75
+ console.error('Usage: node search.js <query> [--json] [--limit N]');
76
+ process.exit(1);
77
+ }
78
+
79
+ try {
80
+ let results;
81
+ const useBrave = !!env.BRAVE_API_KEY;
82
+
83
+ if (useBrave) {
84
+ results = await braveSearch(query);
85
+ } else {
86
+ results = await ddgSearch(query);
87
+ }
88
+
89
+ if (isJson) {
90
+ console.log(JSON.stringify(results, null, 2));
91
+ } else {
92
+ if (results.length === 0) {
93
+ console.log('No results found.');
94
+ } else {
95
+ for (const r of results) {
96
+ console.log(`## ${r.title}`);
97
+ console.log(`**Source:** ${r.source}`);
98
+ console.log(`**URL:** ${r.url}`);
99
+ if (r.description) console.log(`${r.description}`);
100
+ console.log('');
101
+ }
102
+ }
103
+ }
104
+ } catch (err) {
105
+ console.error('Error:', err.message);
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ main();