@xpr-agents/openclaw 0.3.1 → 0.4.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.
Files changed (57) hide show
  1. package/README.md +51 -10
  2. package/openclaw.plugin.json +15 -1
  3. package/package.json +7 -4
  4. package/skills/code-sandbox/SKILL.md +30 -0
  5. package/skills/code-sandbox/dist/index.js +188 -0
  6. package/skills/code-sandbox/skill.json +13 -0
  7. package/skills/code-sandbox/src/index.ts +212 -0
  8. package/skills/creative/SKILL.md +32 -0
  9. package/skills/creative/dist/index.js +667 -0
  10. package/skills/creative/skill.json +13 -0
  11. package/skills/creative/src/index.ts +679 -0
  12. package/skills/defi/SKILL.md +123 -0
  13. package/skills/defi/dist/index.js +1745 -0
  14. package/skills/defi/skill.json +44 -0
  15. package/skills/defi/src/index.ts +1788 -0
  16. package/skills/defi/test-read.mjs +281 -0
  17. package/skills/governance/SKILL.md +69 -0
  18. package/skills/governance/dist/index.js +632 -0
  19. package/skills/governance/skill.json +21 -0
  20. package/skills/governance/src/index.ts +656 -0
  21. package/skills/governance/test-read.mjs +176 -0
  22. package/skills/lending/SKILL.md +63 -0
  23. package/skills/lending/dist/index.js +1039 -0
  24. package/skills/lending/skill.json +29 -0
  25. package/skills/lending/src/index.ts +1105 -0
  26. package/skills/lending/test-read.mjs +156 -0
  27. package/skills/nft/SKILL.md +95 -0
  28. package/skills/nft/dist/index.js +1520 -0
  29. package/skills/nft/skill.json +37 -0
  30. package/skills/nft/src/index.ts +1539 -0
  31. package/skills/shellbook/SKILL.md +59 -0
  32. package/skills/shellbook/dist/index.js +381 -0
  33. package/skills/shellbook/skill.json +29 -0
  34. package/skills/shellbook/src/index.ts +391 -0
  35. package/skills/shellbook/tsconfig.json +14 -0
  36. package/skills/smart-contracts/SKILL.md +128 -0
  37. package/skills/smart-contracts/dist/index.js +1225 -0
  38. package/skills/smart-contracts/skill.json +25 -0
  39. package/skills/smart-contracts/src/index.ts +1327 -0
  40. package/skills/smart-contracts/tsconfig.json +14 -0
  41. package/skills/structured-data/SKILL.md +36 -0
  42. package/skills/structured-data/dist/index.js +501 -0
  43. package/skills/structured-data/skill.json +13 -0
  44. package/skills/structured-data/src/index.ts +597 -0
  45. package/skills/tax/SKILL.md +109 -0
  46. package/skills/tax/dist/index.js +1749 -0
  47. package/skills/tax/skill.json +20 -0
  48. package/skills/tax/src/index.ts +1985 -0
  49. package/skills/web-scraping/SKILL.md +29 -0
  50. package/skills/web-scraping/dist/index.js +311 -0
  51. package/skills/web-scraping/skill.json +13 -0
  52. package/skills/web-scraping/src/index.ts +371 -0
  53. package/skills/xmd/SKILL.md +52 -0
  54. package/skills/xmd/dist/index.js +596 -0
  55. package/skills/xmd/skill.json +22 -0
  56. package/skills/xmd/src/index.ts +635 -0
  57. package/skills/xmd/test-read.mjs +178 -0
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: web-scraping
3
+ description: Web scraping tools for fetching and extracting data from web pages
4
+ ---
5
+
6
+ ## Web Scraping
7
+
8
+ You have web scraping tools for fetching and extracting data from web pages:
9
+
10
+ **Single page:**
11
+ - `scrape_url` — fetch a URL and get cleaned text content + metadata (title, description, link count)
12
+ - Use format="text" (default) for most tasks — strips all HTML
13
+ - Use format="markdown" to preserve headings, links, lists, bold/italic
14
+ - Use format="html" only when you need raw HTML
15
+
16
+ **Link discovery:**
17
+ - `extract_links` — fetch a page and extract all links with text and type (internal/external)
18
+ - Use the `pattern` parameter to filter by regex (e.g. `"\\.pdf$"` for PDF links)
19
+ - Links are deduplicated and resolved to absolute URLs
20
+
21
+ **Multi-page research:**
22
+ - `scrape_multiple` — fetch up to 10 URLs in parallel for comparison/research
23
+ - One failure doesn't block others (uses Promise.allSettled)
24
+
25
+ **Best practices:**
26
+ - Prefer "text" format for content extraction, "markdown" for preserving structure
27
+ - Don't scrape the same domain more than 5 times per minute
28
+ - Combine with `store_deliverable` to save scraped content as job evidence
29
+ - For very large pages, the content is limited to 5MB
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+ /**
3
+ * Web Scraping Skill — fetch, parse, and extract data from web pages
4
+ *
5
+ * Zero external dependencies — uses Node.js built-in fetch and regex-based HTML parsing.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.default = webScrapingSkill;
9
+ // ── Constants ───────────────────────────────────
10
+ const MAX_BODY_SIZE = 5 * 1024 * 1024; // 5MB
11
+ const DEFAULT_TIMEOUT = 30000;
12
+ const USER_AGENT = 'XPR-Agent/1.0 (web-scraping-skill)';
13
+ // ── Shared helpers ──────────────────────────────
14
+ async function fetchPage(url, timeout = DEFAULT_TIMEOUT, headers) {
15
+ const controller = new AbortController();
16
+ const timer = setTimeout(() => controller.abort(), timeout);
17
+ try {
18
+ const resp = await fetch(url, {
19
+ signal: controller.signal,
20
+ redirect: 'follow',
21
+ headers: {
22
+ 'User-Agent': USER_AGENT,
23
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
24
+ ...headers,
25
+ },
26
+ });
27
+ const contentType = resp.headers.get('content-type') || '';
28
+ const contentLength = parseInt(resp.headers.get('content-length') || '0');
29
+ if (contentLength > MAX_BODY_SIZE) {
30
+ throw new Error(`Response too large: ${contentLength} bytes (max ${MAX_BODY_SIZE})`);
31
+ }
32
+ const html = await resp.text();
33
+ if (html.length > MAX_BODY_SIZE) {
34
+ throw new Error(`Response body too large: ${html.length} chars (max ${MAX_BODY_SIZE})`);
35
+ }
36
+ return {
37
+ html,
38
+ status: resp.status,
39
+ contentType: contentType.split(';')[0].trim(),
40
+ finalUrl: resp.url || url,
41
+ };
42
+ }
43
+ finally {
44
+ clearTimeout(timer);
45
+ }
46
+ }
47
+ function decodeEntities(text) {
48
+ return text
49
+ .replace(/&/g, '&')
50
+ .replace(/&lt;/g, '<')
51
+ .replace(/&gt;/g, '>')
52
+ .replace(/&quot;/g, '"')
53
+ .replace(/&#39;/g, "'")
54
+ .replace(/&apos;/g, "'")
55
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n)))
56
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)))
57
+ .replace(/&nbsp;/g, ' ');
58
+ }
59
+ function stripHtml(html) {
60
+ // Remove script, style, and noscript blocks
61
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, '');
62
+ text = text.replace(/<style[\s\S]*?<\/style>/gi, '');
63
+ text = text.replace(/<noscript[\s\S]*?<\/noscript>/gi, '');
64
+ // Remove HTML comments
65
+ text = text.replace(/<!--[\s\S]*?-->/g, '');
66
+ // Remove tags
67
+ text = text.replace(/<[^>]+>/g, ' ');
68
+ // Decode entities
69
+ text = decodeEntities(text);
70
+ // Normalize whitespace
71
+ text = text.replace(/[ \t]+/g, ' ');
72
+ text = text.replace(/\n\s*\n\s*\n/g, '\n\n');
73
+ return text.trim();
74
+ }
75
+ function htmlToMarkdown(html) {
76
+ // Remove script/style/noscript
77
+ let md = html.replace(/<script[\s\S]*?<\/script>/gi, '');
78
+ md = md.replace(/<style[\s\S]*?<\/style>/gi, '');
79
+ md = md.replace(/<noscript[\s\S]*?<\/noscript>/gi, '');
80
+ md = md.replace(/<!--[\s\S]*?-->/g, '');
81
+ // Headings
82
+ md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `# ${stripHtml(c)}\n\n`);
83
+ md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, (_, c) => `## ${stripHtml(c)}\n\n`);
84
+ md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, (_, c) => `### ${stripHtml(c)}\n\n`);
85
+ md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, (_, c) => `#### ${stripHtml(c)}\n\n`);
86
+ md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, (_, c) => `##### ${stripHtml(c)}\n\n`);
87
+ md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, (_, c) => `###### ${stripHtml(c)}\n\n`);
88
+ // Bold / italic
89
+ md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**');
90
+ md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*');
91
+ // Links
92
+ md = md.replace(/<a\s+[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
93
+ const cleanText = stripHtml(text);
94
+ return `[${cleanText}](${href})`;
95
+ });
96
+ // List items
97
+ md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, c) => `- ${stripHtml(c)}\n`);
98
+ // Paragraphs / line breaks
99
+ md = md.replace(/<br\s*\/?>/gi, '\n');
100
+ md = md.replace(/<\/p>/gi, '\n\n');
101
+ md = md.replace(/<p[^>]*>/gi, '');
102
+ // Block-level elements → newlines
103
+ md = md.replace(/<\/(div|section|article|header|footer|main|nav)>/gi, '\n');
104
+ // Remove remaining tags
105
+ md = md.replace(/<[^>]+>/g, '');
106
+ // Decode entities
107
+ md = decodeEntities(md);
108
+ // Clean up whitespace
109
+ md = md.replace(/[ \t]+/g, ' ');
110
+ md = md.replace(/\n{3,}/g, '\n\n');
111
+ return md.trim();
112
+ }
113
+ function extractTitle(html) {
114
+ const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
115
+ return match ? decodeEntities(match[1].trim()) : '';
116
+ }
117
+ function extractMetaDescription(html) {
118
+ const match = html.match(/<meta\s+[^>]*name=["']description["'][^>]*content=["']([^"']*)["'][^>]*>/i)
119
+ || html.match(/<meta\s+[^>]*content=["']([^"']*)["'][^>]*name=["']description["'][^>]*>/i);
120
+ return match ? decodeEntities(match[1].trim()) : '';
121
+ }
122
+ function resolveUrl(href, base) {
123
+ try {
124
+ return new URL(href, base).href;
125
+ }
126
+ catch {
127
+ return href;
128
+ }
129
+ }
130
+ function extractLinksFromHtml(html, baseUrl) {
131
+ const links = [];
132
+ const seen = new Set();
133
+ const re = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
134
+ let match;
135
+ let baseOrigin;
136
+ try {
137
+ baseOrigin = new URL(baseUrl).origin;
138
+ }
139
+ catch {
140
+ baseOrigin = '';
141
+ }
142
+ while ((match = re.exec(html)) !== null) {
143
+ const rawHref = match[1].trim();
144
+ // Skip anchors, javascript:, mailto:, tel:
145
+ if (rawHref.startsWith('#') || rawHref.startsWith('javascript:') || rawHref.startsWith('mailto:') || rawHref.startsWith('tel:'))
146
+ continue;
147
+ const absoluteHref = resolveUrl(rawHref, baseUrl);
148
+ if (seen.has(absoluteHref))
149
+ continue;
150
+ seen.add(absoluteHref);
151
+ const text = stripHtml(match[2]).slice(0, 200);
152
+ let linkType = 'external';
153
+ try {
154
+ if (new URL(absoluteHref).origin === baseOrigin)
155
+ linkType = 'internal';
156
+ }
157
+ catch { /* keep external */ }
158
+ links.push({ href: absoluteHref, text, type: linkType });
159
+ }
160
+ return links;
161
+ }
162
+ // ── Skill entry point ───────────────────────────
163
+ function webScrapingSkill(api) {
164
+ // ── scrape_url ──
165
+ api.registerTool({
166
+ name: 'scrape_url',
167
+ description: [
168
+ 'Fetch a web page and return its text content with metadata.',
169
+ 'Strips HTML tags by default (format="text"). Use format="markdown" to preserve structure,',
170
+ 'or format="html" to get raw HTML. Max response size: 5MB.',
171
+ ].join(' '),
172
+ parameters: {
173
+ type: 'object',
174
+ required: ['url'],
175
+ properties: {
176
+ url: { type: 'string', description: 'URL to fetch' },
177
+ format: { type: 'string', description: '"text" (default), "markdown", or "html"' },
178
+ timeout: { type: 'number', description: 'Request timeout in milliseconds (default 30000)' },
179
+ headers: { type: 'object', description: 'Optional custom HTTP headers' },
180
+ },
181
+ },
182
+ handler: async ({ url, format, timeout, headers }) => {
183
+ if (!url || !/^https?:\/\//i.test(url)) {
184
+ return { error: 'Invalid URL. Must start with http:// or https://' };
185
+ }
186
+ try {
187
+ const page = await fetchPage(url, timeout || DEFAULT_TIMEOUT, headers);
188
+ let text;
189
+ const fmt = (format || 'text').toLowerCase();
190
+ if (fmt === 'html') {
191
+ text = page.html;
192
+ }
193
+ else if (fmt === 'markdown') {
194
+ text = htmlToMarkdown(page.html);
195
+ }
196
+ else {
197
+ text = stripHtml(page.html);
198
+ }
199
+ const linkCount = (page.html.match(/<a\s+[^>]*href=/gi) || []).length;
200
+ return {
201
+ url: page.finalUrl,
202
+ title: extractTitle(page.html),
203
+ description: extractMetaDescription(page.html),
204
+ text,
205
+ links_count: linkCount,
206
+ content_length: text.length,
207
+ status: page.status,
208
+ content_type: page.contentType,
209
+ };
210
+ }
211
+ catch (err) {
212
+ return { error: `Failed to fetch ${url}: ${err.message}` };
213
+ }
214
+ },
215
+ });
216
+ // ── extract_links ──
217
+ api.registerTool({
218
+ name: 'extract_links',
219
+ description: [
220
+ 'Fetch a web page and extract all links with context.',
221
+ 'Returns resolved absolute URLs, link text, and internal/external classification.',
222
+ 'Use pattern parameter to filter links by regex on href.',
223
+ ].join(' '),
224
+ parameters: {
225
+ type: 'object',
226
+ required: ['url'],
227
+ properties: {
228
+ url: { type: 'string', description: 'URL to fetch and extract links from' },
229
+ pattern: { type: 'string', description: 'Optional regex pattern to filter link hrefs' },
230
+ limit: { type: 'number', description: 'Maximum links to return (default 50)' },
231
+ },
232
+ },
233
+ handler: async ({ url, pattern, limit }) => {
234
+ if (!url || !/^https?:\/\//i.test(url)) {
235
+ return { error: 'Invalid URL. Must start with http:// or https://' };
236
+ }
237
+ try {
238
+ const page = await fetchPage(url);
239
+ let links = extractLinksFromHtml(page.html, page.finalUrl);
240
+ if (pattern) {
241
+ try {
242
+ const re = new RegExp(pattern, 'i');
243
+ links = links.filter(l => re.test(l.href));
244
+ }
245
+ catch (err) {
246
+ return { error: `Invalid regex pattern: ${err.message}` };
247
+ }
248
+ }
249
+ const maxLinks = Math.min(limit || 50, 200);
250
+ const total = links.length;
251
+ return {
252
+ url: page.finalUrl,
253
+ links: links.slice(0, maxLinks),
254
+ total_found: total,
255
+ returned: Math.min(total, maxLinks),
256
+ };
257
+ }
258
+ catch (err) {
259
+ return { error: `Failed to fetch ${url}: ${err.message}` };
260
+ }
261
+ },
262
+ });
263
+ // ── scrape_multiple ──
264
+ api.registerTool({
265
+ name: 'scrape_multiple',
266
+ description: [
267
+ 'Fetch multiple URLs in parallel and return their text content.',
268
+ 'Useful for research and comparison tasks. Max 10 URLs per call.',
269
+ 'Uses Promise.allSettled so one failure does not block others.',
270
+ ].join(' '),
271
+ parameters: {
272
+ type: 'object',
273
+ required: ['urls'],
274
+ properties: {
275
+ urls: { type: 'array', description: 'Array of URLs to fetch (max 10)', items: { type: 'string' } },
276
+ format: { type: 'string', description: '"text" (default) or "html"' },
277
+ },
278
+ },
279
+ handler: async ({ urls, format }) => {
280
+ if (!Array.isArray(urls) || urls.length === 0) {
281
+ return { error: 'urls must be a non-empty array of strings' };
282
+ }
283
+ if (urls.length > 10) {
284
+ return { error: 'Maximum 10 URLs per call' };
285
+ }
286
+ const fmt = (format || 'text').toLowerCase();
287
+ const results = await Promise.allSettled(urls.map(async (url) => {
288
+ if (!url || !/^https?:\/\//i.test(url)) {
289
+ throw new Error('Invalid URL');
290
+ }
291
+ const page = await fetchPage(url);
292
+ const text = fmt === 'html' ? page.html : stripHtml(page.html);
293
+ return {
294
+ url: page.finalUrl,
295
+ title: extractTitle(page.html),
296
+ text,
297
+ status: page.status,
298
+ };
299
+ }));
300
+ const items = results.map((r, i) => {
301
+ if (r.status === 'fulfilled') {
302
+ return r.value;
303
+ }
304
+ return { url: urls[i], title: '', text: '', status: 0, error: r.reason?.message || 'Unknown error' };
305
+ });
306
+ const succeeded = items.filter(r => !('error' in r)).length;
307
+ const failed = items.length - succeeded;
308
+ return { results: items, succeeded, failed };
309
+ },
310
+ });
311
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "web-scraping",
3
+ "version": "1.0.0",
4
+ "description": "Fetch, parse, and extract data from web pages using Node.js built-in fetch",
5
+ "author": "xpr-agents",
6
+ "category": "oracle",
7
+ "tags": ["web", "scraping", "fetch", "html", "extract"],
8
+ "capabilities": ["web-scraping", "link-extraction"],
9
+ "tools": ["scrape_url", "extract_links", "scrape_multiple"],
10
+ "requires": {
11
+ "env": []
12
+ }
13
+ }