@xpr-agents/openclaw 0.3.2 → 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 (53) hide show
  1. package/README.md +31 -5
  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/skill.json +13 -0
  6. package/skills/code-sandbox/src/index.ts +212 -0
  7. package/skills/creative/SKILL.md +32 -0
  8. package/skills/creative/skill.json +13 -0
  9. package/skills/creative/src/index.ts +679 -0
  10. package/skills/defi/SKILL.md +123 -0
  11. package/skills/defi/dist/index.js +1 -0
  12. package/skills/defi/skill.json +44 -0
  13. package/skills/defi/src/index.ts +1788 -0
  14. package/skills/defi/test-read.mjs +281 -0
  15. package/skills/governance/SKILL.md +69 -0
  16. package/skills/governance/dist/index.js +632 -0
  17. package/skills/governance/skill.json +21 -0
  18. package/skills/governance/src/index.ts +656 -0
  19. package/skills/governance/test-read.mjs +176 -0
  20. package/skills/lending/SKILL.md +63 -0
  21. package/skills/lending/dist/index.js +1039 -0
  22. package/skills/lending/skill.json +29 -0
  23. package/skills/lending/src/index.ts +1105 -0
  24. package/skills/lending/test-read.mjs +156 -0
  25. package/skills/nft/SKILL.md +95 -0
  26. package/skills/nft/dist/index.js +4 -10
  27. package/skills/nft/skill.json +37 -0
  28. package/skills/nft/src/index.ts +1539 -0
  29. package/skills/shellbook/SKILL.md +59 -0
  30. package/skills/shellbook/skill.json +29 -0
  31. package/skills/shellbook/src/index.ts +391 -0
  32. package/skills/shellbook/tsconfig.json +14 -0
  33. package/skills/smart-contracts/SKILL.md +128 -0
  34. package/skills/smart-contracts/skill.json +25 -0
  35. package/skills/smart-contracts/src/index.ts +1327 -0
  36. package/skills/smart-contracts/tsconfig.json +14 -0
  37. package/skills/structured-data/SKILL.md +36 -0
  38. package/skills/structured-data/dist/index.js +501 -0
  39. package/skills/structured-data/skill.json +13 -0
  40. package/skills/structured-data/src/index.ts +597 -0
  41. package/skills/tax/SKILL.md +109 -0
  42. package/skills/tax/dist/index.js +216 -32
  43. package/skills/tax/skill.json +20 -0
  44. package/skills/tax/src/index.ts +1985 -0
  45. package/skills/web-scraping/SKILL.md +29 -0
  46. package/skills/web-scraping/dist/index.js +311 -0
  47. package/skills/web-scraping/skill.json +13 -0
  48. package/skills/web-scraping/src/index.ts +371 -0
  49. package/skills/xmd/SKILL.md +52 -0
  50. package/skills/xmd/dist/index.js +596 -0
  51. package/skills/xmd/skill.json +22 -0
  52. package/skills/xmd/src/index.ts +635 -0
  53. package/skills/xmd/test-read.mjs +178 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Web Scraping Skill — fetch, parse, and extract data from web pages
3
+ *
4
+ * Zero external dependencies — uses Node.js built-in fetch and regex-based HTML parsing.
5
+ */
6
+
7
+ interface ToolDef {
8
+ name: string;
9
+ description: string;
10
+ parameters: { type: 'object'; required?: string[]; properties: Record<string, unknown> };
11
+ handler: (params: any) => Promise<unknown>;
12
+ }
13
+
14
+ interface SkillApi {
15
+ registerTool(tool: ToolDef): void;
16
+ getConfig(): Record<string, unknown>;
17
+ }
18
+
19
+ // ── Constants ───────────────────────────────────
20
+
21
+ const MAX_BODY_SIZE = 5 * 1024 * 1024; // 5MB
22
+ const DEFAULT_TIMEOUT = 30000;
23
+ const USER_AGENT = 'XPR-Agent/1.0 (web-scraping-skill)';
24
+
25
+ // ── Shared helpers ──────────────────────────────
26
+
27
+ async function fetchPage(
28
+ url: string,
29
+ timeout: number = DEFAULT_TIMEOUT,
30
+ headers?: Record<string, string>,
31
+ ): Promise<{ html: string; status: number; contentType: string; finalUrl: string }> {
32
+ const controller = new AbortController();
33
+ const timer = setTimeout(() => controller.abort(), timeout);
34
+
35
+ try {
36
+ const resp = await fetch(url, {
37
+ signal: controller.signal,
38
+ redirect: 'follow',
39
+ headers: {
40
+ 'User-Agent': USER_AGENT,
41
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
42
+ ...headers,
43
+ },
44
+ });
45
+
46
+ const contentType = resp.headers.get('content-type') || '';
47
+ const contentLength = parseInt(resp.headers.get('content-length') || '0');
48
+ if (contentLength > MAX_BODY_SIZE) {
49
+ throw new Error(`Response too large: ${contentLength} bytes (max ${MAX_BODY_SIZE})`);
50
+ }
51
+
52
+ const html = await resp.text();
53
+ if (html.length > MAX_BODY_SIZE) {
54
+ throw new Error(`Response body too large: ${html.length} chars (max ${MAX_BODY_SIZE})`);
55
+ }
56
+
57
+ return {
58
+ html,
59
+ status: resp.status,
60
+ contentType: contentType.split(';')[0].trim(),
61
+ finalUrl: resp.url || url,
62
+ };
63
+ } finally {
64
+ clearTimeout(timer);
65
+ }
66
+ }
67
+
68
+ function decodeEntities(text: string): string {
69
+ return text
70
+ .replace(/&amp;/g, '&')
71
+ .replace(/&lt;/g, '<')
72
+ .replace(/&gt;/g, '>')
73
+ .replace(/&quot;/g, '"')
74
+ .replace(/&#39;/g, "'")
75
+ .replace(/&apos;/g, "'")
76
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n)))
77
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)))
78
+ .replace(/&nbsp;/g, ' ');
79
+ }
80
+
81
+ function stripHtml(html: string): string {
82
+ // Remove script, style, and noscript blocks
83
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, '');
84
+ text = text.replace(/<style[\s\S]*?<\/style>/gi, '');
85
+ text = text.replace(/<noscript[\s\S]*?<\/noscript>/gi, '');
86
+ // Remove HTML comments
87
+ text = text.replace(/<!--[\s\S]*?-->/g, '');
88
+ // Remove tags
89
+ text = text.replace(/<[^>]+>/g, ' ');
90
+ // Decode entities
91
+ text = decodeEntities(text);
92
+ // Normalize whitespace
93
+ text = text.replace(/[ \t]+/g, ' ');
94
+ text = text.replace(/\n\s*\n\s*\n/g, '\n\n');
95
+ return text.trim();
96
+ }
97
+
98
+ function htmlToMarkdown(html: string): string {
99
+ // Remove script/style/noscript
100
+ let md = html.replace(/<script[\s\S]*?<\/script>/gi, '');
101
+ md = md.replace(/<style[\s\S]*?<\/style>/gi, '');
102
+ md = md.replace(/<noscript[\s\S]*?<\/noscript>/gi, '');
103
+ md = md.replace(/<!--[\s\S]*?-->/g, '');
104
+
105
+ // Headings
106
+ md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `# ${stripHtml(c)}\n\n`);
107
+ md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, (_, c) => `## ${stripHtml(c)}\n\n`);
108
+ md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, (_, c) => `### ${stripHtml(c)}\n\n`);
109
+ md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, (_, c) => `#### ${stripHtml(c)}\n\n`);
110
+ md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, (_, c) => `##### ${stripHtml(c)}\n\n`);
111
+ md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, (_, c) => `###### ${stripHtml(c)}\n\n`);
112
+
113
+ // Bold / italic
114
+ md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**');
115
+ md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*');
116
+
117
+ // Links
118
+ md = md.replace(/<a\s+[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
119
+ const cleanText = stripHtml(text);
120
+ return `[${cleanText}](${href})`;
121
+ });
122
+
123
+ // List items
124
+ md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, c) => `- ${stripHtml(c)}\n`);
125
+
126
+ // Paragraphs / line breaks
127
+ md = md.replace(/<br\s*\/?>/gi, '\n');
128
+ md = md.replace(/<\/p>/gi, '\n\n');
129
+ md = md.replace(/<p[^>]*>/gi, '');
130
+
131
+ // Block-level elements → newlines
132
+ md = md.replace(/<\/(div|section|article|header|footer|main|nav)>/gi, '\n');
133
+
134
+ // Remove remaining tags
135
+ md = md.replace(/<[^>]+>/g, '');
136
+
137
+ // Decode entities
138
+ md = decodeEntities(md);
139
+
140
+ // Clean up whitespace
141
+ md = md.replace(/[ \t]+/g, ' ');
142
+ md = md.replace(/\n{3,}/g, '\n\n');
143
+ return md.trim();
144
+ }
145
+
146
+ function extractTitle(html: string): string {
147
+ const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
148
+ return match ? decodeEntities(match[1].trim()) : '';
149
+ }
150
+
151
+ function extractMetaDescription(html: string): string {
152
+ const match = html.match(/<meta\s+[^>]*name=["']description["'][^>]*content=["']([^"']*)["'][^>]*>/i)
153
+ || html.match(/<meta\s+[^>]*content=["']([^"']*)["'][^>]*name=["']description["'][^>]*>/i);
154
+ return match ? decodeEntities(match[1].trim()) : '';
155
+ }
156
+
157
+ function resolveUrl(href: string, base: string): string {
158
+ try {
159
+ return new URL(href, base).href;
160
+ } catch {
161
+ return href;
162
+ }
163
+ }
164
+
165
+ interface ExtractedLink {
166
+ href: string;
167
+ text: string;
168
+ type: 'internal' | 'external';
169
+ }
170
+
171
+ function extractLinksFromHtml(html: string, baseUrl: string): ExtractedLink[] {
172
+ const links: ExtractedLink[] = [];
173
+ const seen = new Set<string>();
174
+ const re = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
175
+ let match;
176
+
177
+ let baseOrigin: string;
178
+ try {
179
+ baseOrigin = new URL(baseUrl).origin;
180
+ } catch {
181
+ baseOrigin = '';
182
+ }
183
+
184
+ while ((match = re.exec(html)) !== null) {
185
+ const rawHref = match[1].trim();
186
+ // Skip anchors, javascript:, mailto:, tel:
187
+ if (rawHref.startsWith('#') || rawHref.startsWith('javascript:') || rawHref.startsWith('mailto:') || rawHref.startsWith('tel:')) continue;
188
+
189
+ const absoluteHref = resolveUrl(rawHref, baseUrl);
190
+ if (seen.has(absoluteHref)) continue;
191
+ seen.add(absoluteHref);
192
+
193
+ const text = stripHtml(match[2]).slice(0, 200);
194
+ let linkType: 'internal' | 'external' = 'external';
195
+ try {
196
+ if (new URL(absoluteHref).origin === baseOrigin) linkType = 'internal';
197
+ } catch { /* keep external */ }
198
+
199
+ links.push({ href: absoluteHref, text, type: linkType });
200
+ }
201
+
202
+ return links;
203
+ }
204
+
205
+ // ── Skill entry point ───────────────────────────
206
+
207
+ export default function webScrapingSkill(api: SkillApi): void {
208
+ // ── scrape_url ──
209
+ api.registerTool({
210
+ name: 'scrape_url',
211
+ description: [
212
+ 'Fetch a web page and return its text content with metadata.',
213
+ 'Strips HTML tags by default (format="text"). Use format="markdown" to preserve structure,',
214
+ 'or format="html" to get raw HTML. Max response size: 5MB.',
215
+ ].join(' '),
216
+ parameters: {
217
+ type: 'object',
218
+ required: ['url'],
219
+ properties: {
220
+ url: { type: 'string', description: 'URL to fetch' },
221
+ format: { type: 'string', description: '"text" (default), "markdown", or "html"' },
222
+ timeout: { type: 'number', description: 'Request timeout in milliseconds (default 30000)' },
223
+ headers: { type: 'object', description: 'Optional custom HTTP headers' },
224
+ },
225
+ },
226
+ handler: async ({ url, format, timeout, headers }: {
227
+ url: string; format?: string; timeout?: number; headers?: Record<string, string>;
228
+ }) => {
229
+ if (!url || !/^https?:\/\//i.test(url)) {
230
+ return { error: 'Invalid URL. Must start with http:// or https://' };
231
+ }
232
+
233
+ try {
234
+ const page = await fetchPage(url, timeout || DEFAULT_TIMEOUT, headers);
235
+
236
+ let text: string;
237
+ const fmt = (format || 'text').toLowerCase();
238
+ if (fmt === 'html') {
239
+ text = page.html;
240
+ } else if (fmt === 'markdown') {
241
+ text = htmlToMarkdown(page.html);
242
+ } else {
243
+ text = stripHtml(page.html);
244
+ }
245
+
246
+ const linkCount = (page.html.match(/<a\s+[^>]*href=/gi) || []).length;
247
+
248
+ return {
249
+ url: page.finalUrl,
250
+ title: extractTitle(page.html),
251
+ description: extractMetaDescription(page.html),
252
+ text,
253
+ links_count: linkCount,
254
+ content_length: text.length,
255
+ status: page.status,
256
+ content_type: page.contentType,
257
+ };
258
+ } catch (err: any) {
259
+ return { error: `Failed to fetch ${url}: ${err.message}` };
260
+ }
261
+ },
262
+ });
263
+
264
+ // ── extract_links ──
265
+ api.registerTool({
266
+ name: 'extract_links',
267
+ description: [
268
+ 'Fetch a web page and extract all links with context.',
269
+ 'Returns resolved absolute URLs, link text, and internal/external classification.',
270
+ 'Use pattern parameter to filter links by regex on href.',
271
+ ].join(' '),
272
+ parameters: {
273
+ type: 'object',
274
+ required: ['url'],
275
+ properties: {
276
+ url: { type: 'string', description: 'URL to fetch and extract links from' },
277
+ pattern: { type: 'string', description: 'Optional regex pattern to filter link hrefs' },
278
+ limit: { type: 'number', description: 'Maximum links to return (default 50)' },
279
+ },
280
+ },
281
+ handler: async ({ url, pattern, limit }: {
282
+ url: string; pattern?: string; limit?: number;
283
+ }) => {
284
+ if (!url || !/^https?:\/\//i.test(url)) {
285
+ return { error: 'Invalid URL. Must start with http:// or https://' };
286
+ }
287
+
288
+ try {
289
+ const page = await fetchPage(url);
290
+ let links = extractLinksFromHtml(page.html, page.finalUrl);
291
+
292
+ if (pattern) {
293
+ try {
294
+ const re = new RegExp(pattern, 'i');
295
+ links = links.filter(l => re.test(l.href));
296
+ } catch (err: any) {
297
+ return { error: `Invalid regex pattern: ${err.message}` };
298
+ }
299
+ }
300
+
301
+ const maxLinks = Math.min(limit || 50, 200);
302
+ const total = links.length;
303
+
304
+ return {
305
+ url: page.finalUrl,
306
+ links: links.slice(0, maxLinks),
307
+ total_found: total,
308
+ returned: Math.min(total, maxLinks),
309
+ };
310
+ } catch (err: any) {
311
+ return { error: `Failed to fetch ${url}: ${err.message}` };
312
+ }
313
+ },
314
+ });
315
+
316
+ // ── scrape_multiple ──
317
+ api.registerTool({
318
+ name: 'scrape_multiple',
319
+ description: [
320
+ 'Fetch multiple URLs in parallel and return their text content.',
321
+ 'Useful for research and comparison tasks. Max 10 URLs per call.',
322
+ 'Uses Promise.allSettled so one failure does not block others.',
323
+ ].join(' '),
324
+ parameters: {
325
+ type: 'object',
326
+ required: ['urls'],
327
+ properties: {
328
+ urls: { type: 'array', description: 'Array of URLs to fetch (max 10)', items: { type: 'string' } },
329
+ format: { type: 'string', description: '"text" (default) or "html"' },
330
+ },
331
+ },
332
+ handler: async ({ urls, format }: { urls: string[]; format?: string }) => {
333
+ if (!Array.isArray(urls) || urls.length === 0) {
334
+ return { error: 'urls must be a non-empty array of strings' };
335
+ }
336
+ if (urls.length > 10) {
337
+ return { error: 'Maximum 10 URLs per call' };
338
+ }
339
+
340
+ const fmt = (format || 'text').toLowerCase();
341
+
342
+ const results = await Promise.allSettled(
343
+ urls.map(async (url) => {
344
+ if (!url || !/^https?:\/\//i.test(url)) {
345
+ throw new Error('Invalid URL');
346
+ }
347
+ const page = await fetchPage(url);
348
+ const text = fmt === 'html' ? page.html : stripHtml(page.html);
349
+ return {
350
+ url: page.finalUrl,
351
+ title: extractTitle(page.html),
352
+ text,
353
+ status: page.status,
354
+ };
355
+ }),
356
+ );
357
+
358
+ const items = results.map((r, i) => {
359
+ if (r.status === 'fulfilled') {
360
+ return r.value;
361
+ }
362
+ return { url: urls[i], title: '', text: '', status: 0, error: r.reason?.message || 'Unknown error' };
363
+ });
364
+
365
+ const succeeded = items.filter(r => !('error' in r)).length;
366
+ const failed = items.length - succeeded;
367
+
368
+ return { results: items, succeeded, failed };
369
+ },
370
+ });
371
+ }
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: xmd
3
+ description: Metal Dollar (XMD) stablecoin — mint, redeem, supply analytics, collateral reserves, oracle prices
4
+ ---
5
+
6
+ ## Metal Dollar (XMD)
7
+
8
+ You have tools to interact with XMD, XPR Network's native stablecoin. XMD is a multi-collateral stablecoin pegged to $1 USD, minted and redeemed through the `xmd.treasury` contract.
9
+
10
+ ### How XMD Works
11
+
12
+ - **Mint:** Send a supported stablecoin (e.g. XUSDC) to `xmd.treasury` with memo `mint` → receive equivalent XMD at oracle price
13
+ - **Redeem:** Send XMD to `xmd.treasury` with memo `redeem,SYMBOL` (e.g. `redeem,XUSDC`) → receive equivalent stablecoin back
14
+ - **1:1 peg:** Oracle-priced at $1, backed by stablecoin reserves in the treasury
15
+ - **Zero fees:** Currently 0% mint and redemption fees on all collateral types
16
+
17
+ ### Supported Collateral
18
+
19
+ | Token | Contract | Oracle Feed | Max Treasury % | Status |
20
+ |-------|----------|-------------|----------------|--------|
21
+ | XUSDC | xtokens | USDC/USD | 60% | Mint + Redeem |
22
+ | XPAX | xtokens | PAX/USD | 15% | Mint + Redeem |
23
+ | XPYUSD | xtokens | PYUSD/USD | 15% | Mint + Redeem |
24
+ | MPD | mpd.token | MPD/USD | 2% | Mint + Redeem |
25
+
26
+ ### Contracts
27
+
28
+ - `xmd.token` — XMD token contract (precision 6, issuer = xmd.treasury)
29
+ - `xmd.treasury` — Mint/redeem logic, collateral management, oracle integration
30
+ - `oracles` — On-chain price feeds from multiple providers
31
+
32
+ ### Read-Only Tools (safe, no signing)
33
+
34
+ - `xmd_get_config` — treasury config: paused state, fee account, minimum oracle price threshold
35
+ - `xmd_list_collateral` — all supported collateral tokens with fees, limits, oracle prices, mint/redeem volumes
36
+ - `xmd_get_supply` — XMD total circulating supply
37
+ - `xmd_get_balance` — check any account's XMD balance
38
+ - `xmd_get_treasury_reserves` — current stablecoin reserves backing XMD, with USD valuations and collateralization ratio
39
+ - `xmd_get_oracle_price` — current oracle price for any collateral token (with individual provider data)
40
+
41
+ ### Write Tools (require `confirmed: true`)
42
+
43
+ - `xmd_mint` — mint XMD by depositing a supported stablecoin
44
+ - `xmd_redeem` — redeem XMD for a supported stablecoin
45
+
46
+ ### Safety Rules
47
+
48
+ - Oracle price must be >= 0.995 (`minOraclePrice`) for mint/redeem to proceed
49
+ - Each collateral has a `maxTreasuryPercent` cap — if the treasury already holds too much of one stablecoin, minting with it is blocked
50
+ - Check `isMintEnabled` / `isRedeemEnabled` before attempting operations
51
+ - The treasury can be paused by admins (`isPaused`) — check config first
52
+ - XMD has precision 6 — all amounts use 6 decimal places (e.g. `1.000000 XMD`)