Skill: ianwchoi/duckduckgo-websearch (version 1.0.1)

--- name: duckduckgo-websearch description: High-quality web search using DuckDuckGo (Instant Answer + SERP scraping fallback). Use when the user asks to search the web, fetch top results, or summarize web pages. Triggers for: (1) general web queries (news, facts, links), (2) site-specific searches, (3) short summaries of top results. Prefer this skill for private/offline-friendly search without Brave API keys. --- # DuckDuckGo Websearch Skill Purpose - Provide a stable, privacy-minded websearch skill using DuckDuckGo's Instant Answer JSON API as primary source and a lightweight HTML SERP scrape as a fallback for richer link lists. - Return structured results: concise summary (if available), top 5 links with titles and snippets, and optional short summary of a landed page when requested. When to use - Use this skill when the user asks to "search the web", "find links about X", "summarize search results for Y", or any task requiring quick web lookup without paid search APIs. - Prefer the skill when privacy-friendly sources are acceptable and rate limits / API keys for other search providers are unavailable. Design principles - Progressive disclosure: SKILL.md contains the trigger and workflow. Detailed parsing and helpers are in scripts/ so the agent can execute them without loading large text. - Fail gracefully: if Instant Answer returns limited data, the skill falls back to a minimal HTML fetch-and-parse to extract top results. - Safety: avoid returning raw HTML; always sanitize snippets and obey robots (basic checks). Bundled resources - scripts/ddg_search.js — Node script that performs the search and returns JSON (summary, links[]). Executable by the agent when the skill is invoked. - references/usage_examples.md — short examples of prompts that should trigger this skill. Outputs - JSON shape returned from scripts/ddg_search.js: { "query": "...", "summary": "short abstract or empty", "links": [ {"title":"...","url":"...","snippet":"..."}, ... up to 5], "source": "instant-answer|serp-fallback", "notes": "any warnings" } Security & limits - The script uses DuckDuckGo's public instant-answer endpoint (no key). For many queries this is enough; for others the SERP fallback will perform a lightweight HTML request. Respect rate limits and avoid heavy scraping. - Do not follow or fetch pages that explicitly disallow crawling via robots.txt for automation-intended user agents. Installation / Usage (for maintainer) 1. Place this skill in ~/.openclaw/workspace/skills/duckduckgo-websearch 2. Ensure node is available (Node 18+). The script has zero external dependencies (uses built-in https/http and node-html-parser). 3. Test manually: node scripts/ddg_search.js "query terms" Trigger examples (see references/usage_examples.md) - "Search DuckDuckGo for recent news about [topic] and give me top links." - "Find the best tutorials for Django channels and summarize top results." ---

path: _meta.json
content: ownerId: kn7329ajnz29cf6jnktdw8545h81q0yx
slug: duckduckgo-websearch
version: 1.0.0
publishedAt: 1771884898759

path: references/usage_examples.md
content: Trigger examples and expected behavior

- "Search web for 'latest K-POP comeback 2026' and give me top 5 links with short snippets."
  - Skill should return JSON with summary (if available) and 5 links.

- "Find tutorials for Django channels and summarize top resource."
  - Skill should return top links and, if requested, an optional follow-up fetch+summary of the landing page.

- "DuckDuckGo: what's the gist of X?"
  - Skill should prefer Instant Answer summary if present; otherwise return top SERP links.

Notes for callers
- To request a page summary, call the skill for the chosen link and use a separate page-summarize helper (not bundled here) to fetch and summarize the HTML body.
- Keep queries concise. For long, complex research tasks, prefer iterative queries and follow-ups.


path: scripts/ddg_search.js
content: #!/usr/bin/env node
// Lightweight DuckDuckGo websearch script
// Usage: node ddg_search.js "search query"

const https = require('https');
const http = require('http');
const { parse } = require('node-html-parser');
const qs = require('querystring');

function instantAnswer(query, cb) {
  const u = 'https://api.duckduckgo.com/?' + qs.stringify({ q: query, format: 'json', no_html: 1, no_redirect: 1 });
  https.get(u, res => {
    let data = '';
    res.on('data', c => data += c);
    res.on('end', () => {
      try {
        const j = JSON.parse(data);
        cb(null, j);
      } catch (e) { cb(e); }
    });
  }).on('error', cb);
}

function serpFallback(query, cb) {
  const u = 'https://duckduckgo.com/html/?' + qs.stringify({ q: query });
  https.get(u, res => {
    let data = '';
    res.on('data', c => data += c);
    res.on('end', () => {
      try {
        const root = parse(data);
        const results = [];
        // DuckDuckGo HTML uses .result__a for links in modern markup; fallback scanning
        const anchors = root.querySelectorAll('a.result__a');
        if (anchors.length === 0) {
          // older/simple HTML pages
          const items = root.querySelectorAll('.result');
          for (const it of items.slice(0, 10)) {
            const a = it.querySelector('a');
            if (!a) continue;
            const title = a.text.trim();
            const url = a.getAttribute('href');
            const snippetEl = it.querySelector('.result__snippet') || it.querySelector('.snippet') || null;
            const snippet = snippetEl ? snippetEl.text.trim() : '';
            results.push({ title, url, snippet });
          }
        } else {
          for (const a of anchors.slice(0, 10)) {
            const title = a.text.trim();
            const url = a.getAttribute('href');
            // try to find snippet nearby
            const parent = a.parentNode;
            let snippet = '';
            const s = parent.querySelector('.result__snippet');
            if (s) snippet = s.text.trim();
            results.push({ title, url, snippet });
          }
        }
        cb(null, results.slice(0,5));
      } catch (e) { cb(e); }
    });
  }).on('error', cb);
}

function toOutput(query, ia, serp) {
  const out = { query, summary: '', links: [], source: '', notes: '' };
  if (ia && (ia.AbstractText || (ia.RelatedTopics && ia.RelatedTopics.length))) {
    out.summary = (ia.AbstractText || '').trim();
    if (!out.summary && ia.RelatedTopics && ia.RelatedTopics.length) {
      out.summary = ia.RelatedTopics.slice(0,2).map(t => (t.Text||t).toString()).join('\n');
    }
    out.source = 'instant-answer';
    if (ia.Results && ia.Results.length) {
      for (const r of ia.Results.slice(0,5)) {
        out.links.push({ title: r.Text, url: r.FirstURL, snippet: '' });
      }
    }
  }
  if (serp && serp.length && out.links.length < 5) {
    for (const r of serp) {
      if (out.links.length >=5) break;
      out.links.push({ title: r.title, url: r.url, snippet: r.snippet });
    }
    if (!out.source) out.source = 'serp-fallback';
  }
  return out;
}

function run(query) {
  instantAnswer(query, (err, ia) => {
    if (err) ia = null;
    serpFallback(query, (err2, serp) => {
      if (err2) serp = null;
      const out = toOutput(query, ia, serp);
      console.log(JSON.stringify(out, null, 2));
    });
  });
}

if (require.main === module) {
  const q = process.argv.slice(2).join(' ').trim();
  if (!q) { console.error('Usage: ddg_search.js "query"'); process.exit(2); }
  run(q);
}


path: scripts/package-lock.json
content: {
  "name": "scripts",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "scripts",
      "version": "1.0.0",
      "license": "ISC",
      "dependencies": {
        "node-html-parser": "^7.0.2"
      }
    },
    "node_modules/boolbase": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/css-select": {
      "version": "5.2.2",
      "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "boolbase": "^1.0.0",
        "css-what": "^6.1.0",
        "domhandler": "^5.0.2",
        "domutils": "^3.0.1",
        "nth-check": "^2.0.1"
      },
      "funding": {
        "url": "https://github.com/sponsors/fb55"
      }
    },
    "node_modules/css-what": {
      "version": "6.2.2",
      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">= 6"
      },
      "funding": {
        "url": "https://github.com/sponsors/fb55"
      }
    },
    "node_modules/dom-serializer": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "domelementtype": "^2.3.0",
        "domhandler": "^5.0.2",
        "entities": "^4.2.0"
      },
      "funding": {
        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
      }
    },
    "node_modules/domelementtype": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/fb55"
        }
      ],
      "license": "BSD-2-Clause"
    },
    "node_modules/domhandler": {
      "version": "5.0.3",
      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "domelementtype": "^2.3.0"
      },
      "engines": {
        "node": ">= 4"
      },
      "funding": {
        "url": "https://github.com/fb55/domhandler?sponsor=1"
      }
    },
    "node_modules/domutils": {
      "version": "3.2.2",
      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "dom-serializer": "^2.0.0",
        "domelementtype": "^2.3.0",
        "domhandler": "^5.0.3"
      },
      "funding": {
        "url": "https://github.com/fb55/domutils?sponsor=1"
      }
    },
    "node_modules/entities": {
      "version": "4.5.0",
      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.12"
      },
      "funding": {
        "url": "https://github.com/fb55/entities?sponsor=1"
      }
    },
    "node_modules/he": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "bin": {
        "he": "bin/he"
      }
    },
    "node_modules/node-html-parser": {
      "version": "7.0.2",
      "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "css-select": "^5.1.0",
        "he": "1.2.0"
      }
    },
    "node_modules/nth-check": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "boolbase": "^1.0.0"
      },
      "funding": {
        "url": "https://github.com/fb55/nth-check?sponsor=1"
      }
    }
  }
}


path: scripts/package.json
content: {
  "name": "scripts",
  "version": "1.0.0",
  "description": "",
  "main": "ddg_search.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "node-html-parser": "^7.0.2"
  }
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)