@getfoundry/unbrowse-openclaw 0.3.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/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # Unbrowse
2
+
3
+ **100x faster web access for OpenClaw agents.**
4
+
5
+ ## The Problem
6
+
7
+ AI agents like OpenClaw need to interact with websites. Today, they have two options:
8
+
9
+ 1. **Official APIs** — Fast and reliable, but only ~1% of websites have them
10
+ 2. **Browser automation** — Universal but painfully slow (5-60 seconds per action)
11
+
12
+ Browser automation means launching headless Chrome, waiting for pages to load, finding elements with fragile CSS selectors, clicking buttons, waiting for navigation, parsing DOM... all to get data that's already available as clean JSON in the network layer.
13
+
14
+ ```
15
+ Browser Automation Reality
16
+ ──────────────────────────
17
+ Launch browser 3-5s
18
+ Navigate to page 2-10s
19
+ Wait for load 1-5s
20
+ Find element 0.5-2s
21
+ Click/interact 0.5-1s
22
+ Wait for response 2-10s
23
+ Parse DOM 1-3s
24
+ ──────────────────────────
25
+ Total: 10-40 seconds per action
26
+ Success rate: 70-85%
27
+ ```
28
+
29
+ ## The Solution
30
+
31
+ Every website has internal APIs — the XHR/fetch calls their frontend makes to load data. These endpoints are undocumented but return clean JSON, handle auth properly, and are 50-100x faster than browser automation.
32
+
33
+ **Unbrowse captures these internal APIs and turns them into skills your agent can call directly.**
34
+
35
+ ```
36
+ Unbrowse
37
+ ────────
38
+ Direct API call → JSON response
39
+ ────────
40
+ Total: 200-500ms
41
+ Success rate: 95%+
42
+ ```
43
+
44
+ ## How It Works
45
+
46
+ ### 1. You browse normally
47
+
48
+ Login to Twitter, scroll your feed, like a post. Unbrowse captures every API call the frontend makes:
49
+
50
+ - `GET /2/timeline/home.json` — fetches your feed
51
+ - `POST /2/timeline/like.json` — likes a tweet
52
+ - Auth headers, cookies, rate limits — all recorded
53
+
54
+ ### 2. Unbrowse generates a skill
55
+
56
+ From captured traffic, Unbrowse creates a callable skill:
57
+
58
+ ```typescript
59
+ // Generated automatically
60
+ twitter.getTimeline() // 200ms, returns JSON
61
+ twitter.likeTweet(id) // 150ms, returns status
62
+ twitter.postTweet(text) // 180ms, returns tweet
63
+ ```
64
+
65
+ ### 3. Your agent uses it forever
66
+
67
+ No browser. No waiting. No fragile selectors. Just direct API calls at network speed.
68
+
69
+ ```
70
+ ┌─────────────────────────────────────────────────────────────┐
71
+ │ BEFORE UNBROWSE │
72
+ │ │
73
+ │ Agent → Launch browser → Load page → Find element → │
74
+ │ Click → Wait → Parse DOM → Extract data │
75
+ │ │
76
+ │ Time: 30 seconds Success: 75% │
77
+ ├─────────────────────────────────────────────────────────────┤
78
+ │ WITH UNBROWSE │
79
+ │ │
80
+ │ Agent → API call → JSON response │
81
+ │ │
82
+ │ Time: 300ms Success: 95%+ │
83
+ └─────────────────────────────────────────────────────────────┘
84
+ ```
85
+
86
+ ## What is OpenClaw?
87
+
88
+ [OpenClaw](https://openclaw.ai) is an open-source AI assistant that runs locally on your devices. It connects to your messaging apps (WhatsApp, Telegram, Slack, Discord, iMessage) and acts as a proactive digital assistant — managing emails, updating calendars, running commands, and taking autonomous actions across your online life.
89
+
90
+ Unlike cloud-based assistants, OpenClaw:
91
+ - Runs on your hardware (your data stays local)
92
+ - Has persistent memory across sessions
93
+ - Can execute shell commands and scripts
94
+ - Extends via community-built skills and plugins
95
+
96
+ **Unbrowse is an OpenClaw extension that gives your agent fast, reliable web access without browser automation overhead.**
97
+
98
+ ## Installation
99
+
100
+ ```bash
101
+ openclaw plugins install @getfoundry/unbrowse-openclaw
102
+ ```
103
+
104
+ Also works on Clawdbot and Moltbot:
105
+ ```bash
106
+ clawdbot plugins install @getfoundry/unbrowse-openclaw
107
+ moltbot plugins install @getfoundry/unbrowse-openclaw
108
+ ```
109
+
110
+ ## Quick Start
111
+
112
+ ### Capture APIs
113
+
114
+ ```bash
115
+ # Tell your agent to browse a site
116
+ "Browse twitter.com and capture the API"
117
+
118
+ # Or use the tool directly
119
+ unbrowse_capture url="twitter.com"
120
+ ```
121
+
122
+ ### Generate a skill
123
+
124
+ ```bash
125
+ unbrowse_generate_skill domain="twitter.com"
126
+ ```
127
+
128
+ ### Use it
129
+
130
+ ```bash
131
+ # Your agent can now call Twitter's internal API directly
132
+ unbrowse_replay skill="twitter" action="get_timeline"
133
+ ```
134
+
135
+ ## Tools
136
+
137
+ ### Capture & Generate
138
+
139
+ | Tool | Description |
140
+ |------|-------------|
141
+ | `unbrowse_browse` | Open URL with traffic capture |
142
+ | `unbrowse_capture` | Capture API traffic from domain |
143
+ | `unbrowse_generate_skill` | Generate skill from captured endpoints |
144
+ | `unbrowse_replay` | Execute API calls using skills |
145
+
146
+ ### Auth & Sessions
147
+
148
+ | Tool | Description |
149
+ |------|-------------|
150
+ | `unbrowse_login` | Login and save session |
151
+ | `unbrowse_session` | Manage saved sessions |
152
+ | `unbrowse_cookies` | Export cookies for a domain |
153
+
154
+ ### Marketplace
155
+
156
+ | Tool | Description |
157
+ |------|-------------|
158
+ | `unbrowse_search` | Find skills others have created |
159
+ | `unbrowse_install` | Install a skill (own it forever) |
160
+ | `unbrowse_publish` | Share your skills |
161
+
162
+ ## Skill Marketplace
163
+
164
+ Don't want to capture APIs yourself? Browse skills others have already created.
165
+
166
+ ```bash
167
+ # Search for Twitter skills
168
+ unbrowse_search query="twitter"
169
+
170
+ # Install one (you own it forever)
171
+ unbrowse_install skill="twitter-timeline"
172
+ ```
173
+
174
+ ### Publish Your Own
175
+
176
+ Share captured APIs with other agents:
177
+
178
+ ```bash
179
+ # Free
180
+ unbrowse_publish name="twitter-timeline"
181
+
182
+ # Paid ($2.50 USDC)
183
+ unbrowse_publish name="twitter-timeline" price="2.50"
184
+ ```
185
+
186
+ **Earnings:** 70% to creator, 30% platform. Instant payout via x402 protocol on Solana.
187
+
188
+ **Ownership model:** Buyers own skills forever. One purchase, unlimited use. No per-execution fees.
189
+
190
+ ## Configuration
191
+
192
+ ```json
193
+ {
194
+ "plugins": {
195
+ "entries": {
196
+ "unbrowse-openclaw": {
197
+ "enabled": true,
198
+ "config": {
199
+ "skillsOutputDir": "~/.openclaw/skills",
200
+ "autoDiscover": true,
201
+ "creatorWallet": "YOUR_SOLANA_ADDRESS"
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+ ```
208
+
209
+ | Option | Default | Description |
210
+ |--------|---------|-------------|
211
+ | `skillsOutputDir` | `~/.openclaw/skills` | Where skills are saved |
212
+ | `autoDiscover` | `true` | Auto-generate skills while browsing |
213
+ | `creatorWallet` | - | Solana address for marketplace earnings |
214
+ | `credentialSource` | `none` | Password lookup: none/keychain/1password |
215
+
216
+ ## Why Not Just Use Browser Automation?
217
+
218
+ | | Browser Automation | Unbrowse |
219
+ |---|---|---|
220
+ | **Speed** | 10-40 seconds | 200-500ms |
221
+ | **Reliability** | 70-85% (DOM changes break selectors) | 95%+ (APIs rarely change) |
222
+ | **Resource usage** | High (headless browser) | Minimal (HTTP calls) |
223
+ | **Auth handling** | Complex (cookies, sessions, CAPTCHAs) | Built-in (captured with traffic) |
224
+ | **Data format** | Parse from DOM | Clean JSON |
225
+
226
+ Browser automation has its place — when you need to interact with sites that truly require JavaScript rendering. But most "automation" is just fetching data that's already available via internal APIs.
227
+
228
+ ## x402 Payments
229
+
230
+ Skill purchases use the [x402 protocol](https://x402.org) for machine-to-machine payments:
231
+
232
+ 1. Agent requests skill download
233
+ 2. Server returns HTTP 402 with payment requirements
234
+ 3. Agent signs USDC transaction on Solana
235
+ 4. Server verifies, returns skill
236
+
237
+ No intermediaries. Direct creator payment. Instant settlement.
238
+
239
+ ---
240
+
241
+ **Skip the browser. Call the API.**
242
+
243
+ *Built for [OpenClaw](https://openclaw.ai). Powered by [x402](https://x402.org).*
@@ -0,0 +1,58 @@
1
+ {
2
+ "id": "unbrowse-openclaw",
3
+ "uiHints": {
4
+ "skillsOutputDir": {
5
+ "label": "Skills Output Directory",
6
+ "help": "Where generated skills are saved",
7
+ "placeholder": "~/.openclaw/skills"
8
+ },
9
+ "browserPort": {
10
+ "label": "Browser Control Port",
11
+ "help": "Port for browser CDP control API (default: 18791)"
12
+ },
13
+ "browserUseApiKey": {
14
+ "label": "Browser Use API Key",
15
+ "help": "API key for stealth cloud browsers (browser-use.com). Enables unbrowse_stealth tool.",
16
+ "sensitive": true,
17
+ "placeholder": "bu_..."
18
+ },
19
+ "autoDiscover": {
20
+ "label": "Auto-Discover Skills",
21
+ "help": "Automatically generate skills when the agent browses APIs (default: true)"
22
+ },
23
+ "skillIndexUrl": {
24
+ "label": "Skill Index URL",
25
+ "help": "Cloud skill marketplace API URL",
26
+ "placeholder": "https://skills.unbrowse.ai"
27
+ },
28
+ "creatorWallet": {
29
+ "label": "Creator Wallet (Solana)",
30
+ "help": "Your Solana wallet address for receiving USDC when others download your skills",
31
+ "placeholder": "ABC123..."
32
+ },
33
+ "skillIndexSolanaPrivateKey": {
34
+ "label": "Payment Private Key (Solana)",
35
+ "help": "Base58-encoded Solana private key for paying x402 USDC when downloading skills from the index",
36
+ "sensitive": true,
37
+ "placeholder": "base58..."
38
+ },
39
+ "credentialSource": {
40
+ "label": "Credential Source (Login Passwords)",
41
+ "help": "Opt-in: where to look up login passwords for auto-login. 'keychain' = macOS Keychain, '1password' = 1Password CLI, 'vault' = local encrypted vault, 'none' = disabled (default)"
42
+ }
43
+ },
44
+ "configSchema": {
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "properties": {
48
+ "skillsOutputDir": { "type": "string" },
49
+ "browserPort": { "type": "number", "default": 18791 },
50
+ "browserUseApiKey": { "type": "string" },
51
+ "autoDiscover": { "type": "boolean", "default": true },
52
+ "skillIndexUrl": { "type": "string", "default": "https://skills.unbrowse.ai" },
53
+ "creatorWallet": { "type": "string" },
54
+ "skillIndexSolanaPrivateKey": { "type": "string" },
55
+ "credentialSource": { "type": "string", "enum": ["none", "auto", "keychain", "1password", "vault"], "default": "none" }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Unbrowse — Reverse-engineer internal APIs from any website.
3
+ *
4
+ * Pure native Rust implementation. All functionality provided by unbrowse-native.
5
+ */
6
+ export * from "./native/index.js";
7
+ export default function unbrowsePlugin(api: any): {
8
+ name: string;
9
+ version: any;
10
+ native: any;
11
+ };
package/dist/index.js ADDED
@@ -0,0 +1,523 @@
1
+ /**
2
+ * Unbrowse — Reverse-engineer internal APIs from any website.
3
+ *
4
+ * Pure native Rust implementation. All functionality provided by unbrowse-native.
5
+ */
6
+ // @ts-nocheck - Native module types handled separately
7
+ import * as native from "./native/index.js";
8
+ // Re-export native module
9
+ export * from "./native/index.js";
10
+ export default function unbrowsePlugin(api) {
11
+ // =========================================================================
12
+ // Tool: unbrowse_capture
13
+ // =========================================================================
14
+ api.registerTool({
15
+ name: "unbrowse_capture",
16
+ description: `Capture internal API traffic from browser and generate a skill.
17
+
18
+ Visit URLs in the browser, capture all API calls, extract auth tokens, and generate a reusable skill package.
19
+
20
+ Returns: Skill with endpoints, auth method, and generated TypeScript client.`,
21
+ parameters: {
22
+ type: "object",
23
+ properties: {
24
+ urls: {
25
+ type: "array",
26
+ items: { type: "string" },
27
+ description: "URLs to visit and capture API traffic from",
28
+ },
29
+ output_dir: {
30
+ type: "string",
31
+ description: "Optional output directory for skill files",
32
+ },
33
+ },
34
+ required: ["urls"],
35
+ },
36
+ async execute(args) {
37
+ const apiData = await native.captureFromUrls(args.urls, undefined);
38
+ const result = native.generateSkill(apiData, args.output_dir, undefined);
39
+ return {
40
+ success: true,
41
+ service: result.service,
42
+ skill_dir: result.skillDir,
43
+ endpoints_count: result.endpointsCount,
44
+ auth_method: result.authMethod,
45
+ message: `Captured ${result.endpointsCount} endpoints from ${result.service}. Skill saved to ${result.skillDir}`,
46
+ };
47
+ },
48
+ });
49
+ // =========================================================================
50
+ // Tool: unbrowse_replay
51
+ // =========================================================================
52
+ api.registerTool({
53
+ name: "unbrowse_replay",
54
+ description: `Call an internal API endpoint using captured auth.
55
+
56
+ Execute HTTP requests against internal APIs with proper authentication headers and cookies.
57
+
58
+ Returns: Response status, body, and timing.`,
59
+ parameters: {
60
+ type: "object",
61
+ properties: {
62
+ service: {
63
+ type: "string",
64
+ description: "Service name (skill name) to use for auth",
65
+ },
66
+ method: {
67
+ type: "string",
68
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
69
+ description: "HTTP method",
70
+ },
71
+ path: {
72
+ type: "string",
73
+ description: "API path (e.g., /api/users)",
74
+ },
75
+ body: {
76
+ type: "string",
77
+ description: "Request body (JSON string)",
78
+ },
79
+ },
80
+ required: ["service", "method", "path"],
81
+ },
82
+ async execute(args) {
83
+ const skillInfo = native.getSkillInfo(args.service);
84
+ if (!skillInfo) {
85
+ return { success: false, error: `Skill not found: ${args.service}` };
86
+ }
87
+ const vaultEntry = await native.vaultGet(args.service);
88
+ const authHeaders = vaultEntry?.headers || {};
89
+ const cookies = vaultEntry?.cookies || {};
90
+ const baseUrl = vaultEntry?.baseUrl || `https://${args.service}`;
91
+ const result = await native.testEndpoint(baseUrl, args.method, args.path, authHeaders, cookies, 30000);
92
+ return {
93
+ success: result.status >= 200 && result.status < 400,
94
+ status: result.status,
95
+ latency_ms: result.latencyMs,
96
+ response_shape: result.responseShape,
97
+ response_size: result.responseSize,
98
+ error: result.error,
99
+ };
100
+ },
101
+ });
102
+ // =========================================================================
103
+ // Tool: unbrowse_login
104
+ // =========================================================================
105
+ api.registerTool({
106
+ name: "unbrowse_login",
107
+ description: `Login to a website and capture session auth.
108
+
109
+ Navigates to login page, fills credentials, and captures resulting session cookies/tokens.
110
+
111
+ Returns: Captured auth headers and cookies.`,
112
+ parameters: {
113
+ type: "object",
114
+ properties: {
115
+ url: {
116
+ type: "string",
117
+ description: "Login page URL",
118
+ },
119
+ username: {
120
+ type: "string",
121
+ description: "Username or email (optional - will lookup from keychain)",
122
+ },
123
+ password: {
124
+ type: "string",
125
+ description: "Password (optional - will lookup from keychain)",
126
+ },
127
+ },
128
+ required: ["url"],
129
+ },
130
+ async execute(args) {
131
+ const domain = new URL(args.url).hostname;
132
+ let username = args.username;
133
+ let password = args.password;
134
+ if (!username || !password) {
135
+ const creds = native.lookupCredentials(domain);
136
+ if (creds) {
137
+ username = username || creds.username;
138
+ password = password || creds.password;
139
+ }
140
+ }
141
+ if (!username || !password) {
142
+ return { success: false, error: "Credentials not provided and not found in keychain" };
143
+ }
144
+ await native.browserStart(undefined);
145
+ await native.browserNavigate(args.url, undefined);
146
+ await new Promise(r => setTimeout(r, 2000));
147
+ const snapshot = await native.browserSnapshot(undefined);
148
+ for (const el of snapshot.elements) {
149
+ const elType = el.elementType?.toLowerCase() || "";
150
+ const elName = el.name?.toLowerCase() || "";
151
+ if (elType === "email" || elType === "text" || elName.includes("user") || elName.includes("email")) {
152
+ await native.browserAct("type", el.index, username, undefined);
153
+ }
154
+ if (elType === "password" || elName.includes("pass")) {
155
+ await native.browserAct("type", el.index, password, undefined);
156
+ }
157
+ }
158
+ for (const el of snapshot.elements) {
159
+ const elType = el.elementType?.toLowerCase() || "";
160
+ const elText = el.text?.toLowerCase() || "";
161
+ if (elType === "submit" || elText.includes("sign in") || elText.includes("log in")) {
162
+ await native.browserAct("click", el.index, undefined, undefined);
163
+ break;
164
+ }
165
+ }
166
+ await new Promise(r => setTimeout(r, 3000));
167
+ const authJson = await native.extractBrowserAuth(domain, undefined);
168
+ await native.vaultStore(authJson.service, authJson.baseUrl, authJson.authMethod, authJson.headers || {}, authJson.cookies || {});
169
+ return {
170
+ success: true,
171
+ service: authJson.service,
172
+ auth_method: authJson.authMethod,
173
+ headers_count: Object.keys(authJson.headers || {}).length,
174
+ cookies_count: Object.keys(authJson.cookies || {}).length,
175
+ message: `Logged in and captured auth for ${authJson.service}`,
176
+ };
177
+ },
178
+ });
179
+ // =========================================================================
180
+ // Tool: unbrowse_learn
181
+ // =========================================================================
182
+ api.registerTool({
183
+ name: "unbrowse_learn",
184
+ description: `Parse a HAR file and generate an API skill.
185
+
186
+ Takes a HAR file (from browser DevTools export) and generates a complete skill package.
187
+
188
+ Returns: Generated skill with endpoints and auth.`,
189
+ parameters: {
190
+ type: "object",
191
+ properties: {
192
+ har_path: {
193
+ type: "string",
194
+ description: "Path to HAR file",
195
+ },
196
+ seed_url: {
197
+ type: "string",
198
+ description: "Seed URL to determine service name",
199
+ },
200
+ output_dir: {
201
+ type: "string",
202
+ description: "Optional output directory",
203
+ },
204
+ },
205
+ required: ["har_path"],
206
+ },
207
+ async execute(args) {
208
+ const fs = await import("node:fs");
209
+ if (!fs.existsSync(args.har_path)) {
210
+ return { success: false, error: `HAR file not found: ${args.har_path}` };
211
+ }
212
+ const harJson = fs.readFileSync(args.har_path, "utf-8");
213
+ const apiData = native.parseHar(harJson, args.seed_url);
214
+ const result = native.generateSkill(apiData, args.output_dir, undefined);
215
+ await native.vaultStore(apiData.service, apiData.baseUrl, apiData.authMethod, apiData.authHeaders, apiData.cookies);
216
+ return {
217
+ success: true,
218
+ service: result.service,
219
+ skill_dir: result.skillDir,
220
+ endpoints_count: result.endpointsCount,
221
+ auth_method: result.authMethod,
222
+ };
223
+ },
224
+ });
225
+ // =========================================================================
226
+ // Tool: unbrowse_skills
227
+ // =========================================================================
228
+ api.registerTool({
229
+ name: "unbrowse_skills",
230
+ description: `List all captured API skills.
231
+
232
+ Shows locally learned skills with their endpoints and auth methods.`,
233
+ parameters: {
234
+ type: "object",
235
+ properties: {},
236
+ },
237
+ async execute() {
238
+ const skills = native.listSkills();
239
+ const details = skills.map((service) => {
240
+ const info = native.getSkillInfo(service);
241
+ return {
242
+ service,
243
+ name: info?.name,
244
+ endpoints: info?.endpointsCount || 0,
245
+ version: info?.version,
246
+ };
247
+ });
248
+ return { success: true, count: skills.length, skills: details };
249
+ },
250
+ });
251
+ // =========================================================================
252
+ // Tool: unbrowse_auth
253
+ // =========================================================================
254
+ api.registerTool({
255
+ name: "unbrowse_auth",
256
+ description: `Extract auth from current browser session.
257
+
258
+ Captures cookies, localStorage, and request headers from the browser.`,
259
+ parameters: {
260
+ type: "object",
261
+ properties: {
262
+ domain: {
263
+ type: "string",
264
+ description: "Domain to extract auth for",
265
+ },
266
+ },
267
+ required: ["domain"],
268
+ },
269
+ async execute(args) {
270
+ const authJson = await native.extractBrowserAuth(args.domain, undefined);
271
+ await native.vaultStore(authJson.service, authJson.baseUrl, authJson.authMethod, authJson.headers || {}, authJson.cookies || {});
272
+ return {
273
+ success: true,
274
+ service: authJson.service,
275
+ auth_method: authJson.authMethod,
276
+ base_url: authJson.baseUrl,
277
+ headers: Object.keys(authJson.headers || {}),
278
+ cookies: Object.keys(authJson.cookies || {}),
279
+ };
280
+ },
281
+ });
282
+ // =========================================================================
283
+ // Tool: unbrowse_publish
284
+ // =========================================================================
285
+ api.registerTool({
286
+ name: "unbrowse_publish",
287
+ description: `Publish a skill to the marketplace.
288
+
289
+ Shares your API skill for others to use. Credentials are stripped before publishing.`,
290
+ parameters: {
291
+ type: "object",
292
+ properties: {
293
+ service: {
294
+ type: "string",
295
+ description: "Service name to publish",
296
+ },
297
+ description: {
298
+ type: "string",
299
+ description: "Description of the skill",
300
+ },
301
+ tags: {
302
+ type: "array",
303
+ items: { type: "string" },
304
+ description: "Tags for discoverability",
305
+ },
306
+ price_usdc: {
307
+ type: "number",
308
+ description: "Price in USDC (0 for free)",
309
+ },
310
+ },
311
+ required: ["service"],
312
+ },
313
+ async execute(args) {
314
+ const fs = await import("node:fs");
315
+ const path = await import("node:path");
316
+ const os = await import("node:os");
317
+ const skillDir = path.join(os.homedir(), ".openclaw", "skills", args.service);
318
+ const skillMdPath = path.join(skillDir, "SKILL.md");
319
+ const apiTsPath = path.join(skillDir, "scripts", "api.ts");
320
+ const authJsonPath = path.join(skillDir, "auth.json");
321
+ if (!fs.existsSync(skillMdPath)) {
322
+ return { success: false, error: `Skill not found: ${args.service}` };
323
+ }
324
+ const skillMd = fs.readFileSync(skillMdPath, "utf-8");
325
+ const apiTs = fs.existsSync(apiTsPath) ? fs.readFileSync(apiTsPath, "utf-8") : undefined;
326
+ const authJson = fs.existsSync(authJsonPath) ? fs.readFileSync(authJsonPath, "utf-8") : "{}";
327
+ const payload = native.prepareForPublish(skillMd, apiTs, authJson);
328
+ payload.description = args.description;
329
+ payload.tags = args.tags;
330
+ payload.priceUsdc = args.price_usdc;
331
+ const wallet = native.walletGetOrCreate();
332
+ const message = JSON.stringify({ service: args.service, timestamp: Date.now() });
333
+ const signature = native.walletSign(message);
334
+ const result = await native.marketplacePublish(payload, wallet.pubkey, signature, undefined);
335
+ return {
336
+ success: true,
337
+ id: result.id,
338
+ name: result.name,
339
+ service: result.service,
340
+ };
341
+ },
342
+ });
343
+ // =========================================================================
344
+ // Tool: unbrowse_search
345
+ // =========================================================================
346
+ api.registerTool({
347
+ name: "unbrowse_search",
348
+ description: `Search the skill marketplace.
349
+
350
+ Find API skills others have created and shared.`,
351
+ parameters: {
352
+ type: "object",
353
+ properties: {
354
+ query: {
355
+ type: "string",
356
+ description: "Search query",
357
+ },
358
+ },
359
+ required: ["query"],
360
+ },
361
+ async execute(args) {
362
+ const results = await native.marketplaceSearch(args.query, undefined);
363
+ return {
364
+ success: true,
365
+ count: results.length,
366
+ skills: results.map((s) => ({
367
+ id: s.id,
368
+ name: s.name,
369
+ service: s.service,
370
+ description: s.description,
371
+ author: s.author,
372
+ endpoints: s.endpointsCount,
373
+ installs: s.installs,
374
+ price_usdc: s.priceUsdc,
375
+ badge: s.badge,
376
+ })),
377
+ };
378
+ },
379
+ });
380
+ // =========================================================================
381
+ // Tool: unbrowse_download
382
+ // =========================================================================
383
+ api.registerTool({
384
+ name: "unbrowse_download",
385
+ description: `Download a skill from the marketplace.
386
+
387
+ Install a skill locally. May require x402 payment for paid skills.`,
388
+ parameters: {
389
+ type: "object",
390
+ properties: {
391
+ skill_id: {
392
+ type: "string",
393
+ description: "Skill ID to download",
394
+ },
395
+ },
396
+ required: ["skill_id"],
397
+ },
398
+ async execute(args) {
399
+ const fs = await import("node:fs");
400
+ const path = await import("node:path");
401
+ const os = await import("node:os");
402
+ const skillInfo = await native.marketplaceGetSkill(args.skill_id, undefined);
403
+ if (!skillInfo) {
404
+ return { success: false, error: `Skill not found: ${args.skill_id}` };
405
+ }
406
+ let paymentSig;
407
+ if (skillInfo.priceUsdc && skillInfo.priceUsdc > 0 && skillInfo.authorWallet) {
408
+ paymentSig = native.walletSignPayment(args.skill_id, skillInfo.priceUsdc, skillInfo.authorWallet);
409
+ }
410
+ const pkg = await native.marketplaceDownload(args.skill_id, paymentSig, undefined);
411
+ const skillDir = path.join(os.homedir(), ".openclaw", "skills", pkg.id);
412
+ fs.mkdirSync(path.join(skillDir, "scripts"), { recursive: true });
413
+ fs.mkdirSync(path.join(skillDir, "references"), { recursive: true });
414
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), pkg.skillMd);
415
+ if (pkg.apiTs) {
416
+ fs.writeFileSync(path.join(skillDir, "scripts", "api.ts"), pkg.apiTs);
417
+ }
418
+ if (pkg.referenceMd) {
419
+ fs.writeFileSync(path.join(skillDir, "references", "REFERENCE.md"), pkg.referenceMd);
420
+ }
421
+ await native.marketplaceTrackInstall(args.skill_id, undefined);
422
+ return {
423
+ success: true,
424
+ id: pkg.id,
425
+ skill_dir: skillDir,
426
+ endpoints: pkg.endpoints.length,
427
+ auth_method: pkg.authMethod,
428
+ };
429
+ },
430
+ });
431
+ // =========================================================================
432
+ // Tool: unbrowse_wallet
433
+ // =========================================================================
434
+ api.registerTool({
435
+ name: "unbrowse_wallet",
436
+ description: `Manage your marketplace wallet.
437
+
438
+ Create or view your Ed25519 wallet for x402 payments.`,
439
+ parameters: {
440
+ type: "object",
441
+ properties: {
442
+ action: {
443
+ type: "string",
444
+ enum: ["get", "create"],
445
+ description: "Action to perform",
446
+ },
447
+ },
448
+ required: ["action"],
449
+ },
450
+ async execute(args) {
451
+ if (args.action === "create") {
452
+ const existing = native.walletGet();
453
+ if (existing) {
454
+ return { success: true, pubkey: existing.pubkey, created_at: existing.createdAt, message: "Wallet already exists" };
455
+ }
456
+ const wallet = native.walletCreate();
457
+ return { success: true, pubkey: wallet.pubkey, created_at: wallet.createdAt, message: "Created new wallet" };
458
+ }
459
+ else {
460
+ const wallet = native.walletGet();
461
+ if (!wallet) {
462
+ return { success: false, error: "No wallet found. Use action: create" };
463
+ }
464
+ return { success: true, pubkey: wallet.pubkey, created_at: wallet.createdAt };
465
+ }
466
+ },
467
+ });
468
+ // =========================================================================
469
+ // Tool: unbrowse_record
470
+ // =========================================================================
471
+ api.registerTool({
472
+ name: "unbrowse_record",
473
+ description: `Record a workflow session.
474
+
475
+ Start/stop recording browser interactions to learn multi-step workflows.`,
476
+ parameters: {
477
+ type: "object",
478
+ properties: {
479
+ action: {
480
+ type: "string",
481
+ enum: ["start", "stop", "status"],
482
+ description: "Recording action",
483
+ },
484
+ },
485
+ required: ["action"],
486
+ },
487
+ async execute(args) {
488
+ if (args.action === "start") {
489
+ const id = native.recordingStart();
490
+ return { success: true, session_id: id, message: "Recording started" };
491
+ }
492
+ else if (args.action === "stop") {
493
+ const session = native.recordingStop();
494
+ if (!session) {
495
+ return { success: false, error: "No active recording" };
496
+ }
497
+ const workflow = native.workflowLearn(session);
498
+ return {
499
+ success: true,
500
+ session_id: session.id,
501
+ steps: session.steps.length,
502
+ domains: session.domains,
503
+ workflow_id: workflow.id,
504
+ workflow_name: workflow.name,
505
+ };
506
+ }
507
+ else {
508
+ const isActive = native.recordingIsActive();
509
+ const current = native.recordingCurrent();
510
+ return {
511
+ success: true,
512
+ is_active: isActive,
513
+ session: current ? { id: current.id, steps: current.steps.length, domains: current.domains } : null,
514
+ };
515
+ }
516
+ },
517
+ });
518
+ return {
519
+ name: "unbrowse",
520
+ version: native.getVersion(),
521
+ native: native.isNative(),
522
+ };
523
+ }
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: unbrowse-auto-discover
3
+ description: Auto-generates skills when the agent browses APIs
4
+ metadata:
5
+ openclaw:
6
+ emoji: "🔍"
7
+ events: ["tool_result_persist"]
8
+ export: "default"
9
+ ---
10
+
11
+ # Auto-Discover Hook
12
+
13
+ Watches browser tool usage and automatically generates skills when API traffic is detected.
14
+
15
+ ## How It Works
16
+
17
+ 1. Listens for `tool_result_persist` events from the `browser` and `browse` tools
18
+ 2. Polls captured network requests from the browser control API
19
+ 3. When 5+ API calls to a new domain are detected, auto-generates a skill
20
+ 4. Skills are saved to `~/.openclaw/skills/<service>/`
21
+
22
+ ## Configuration
23
+
24
+ Enable/disable in `~/.openclaw/openclaw.json`:
25
+
26
+ ```json5
27
+ {
28
+ plugins: {
29
+ entries: {
30
+ unbrowse: {
31
+ config: {
32
+ autoDiscover: true // default: true
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## Generated Output
41
+
42
+ For each discovered API:
43
+ - `SKILL.md` — OpenClaw-compatible skill definition
44
+ - `auth.json` — Extracted authentication (headers, cookies)
45
+ - `scripts/api.ts` — TypeScript API client
@@ -0,0 +1,119 @@
1
+ {
2
+ "id": "unbrowse-openclaw",
3
+ "name": "Unbrowse",
4
+ "description": "API reverse engineering for OpenClaw. Capture any API and turn it into monetizable skills for AI agents.",
5
+ "version": "0.2.1",
6
+ "repository": "github:lekt9/unbrowse-openclaw",
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "skillsOutputDir": {
12
+ "type": "string",
13
+ "description": "Directory where generated skills are saved"
14
+ },
15
+ "browserPort": {
16
+ "type": "number",
17
+ "default": 18791,
18
+ "description": "Port for browser CDP control API"
19
+ },
20
+ "autoDiscover": {
21
+ "type": "boolean",
22
+ "default": true,
23
+ "description": "Automatically generate skills when the agent browses APIs"
24
+ },
25
+ "skillIndexUrl": {
26
+ "type": "string",
27
+ "default": "https://index.unbrowse.ai",
28
+ "description": "Cloud skill marketplace API URL"
29
+ },
30
+ "marketplace": {
31
+ "type": "object",
32
+ "properties": {
33
+ "creatorWallet": {
34
+ "type": "string",
35
+ "description": "Solana wallet address for receiving USDC payments"
36
+ },
37
+ "solanaPrivateKey": {
38
+ "type": "string",
39
+ "description": "Base58-encoded Solana private key for x402 payments"
40
+ },
41
+ "defaultPrice": {
42
+ "type": "string",
43
+ "default": "0",
44
+ "description": "Default price for published skills (0 = free, or $0.10-$100 USDC)"
45
+ }
46
+ }
47
+ },
48
+ "browser": {
49
+ "type": "object",
50
+ "properties": {
51
+ "useApiKey": {
52
+ "type": "string",
53
+ "description": "Browser Use API key for stealth cloud browsers"
54
+ },
55
+ "proxyCountry": {
56
+ "type": "string",
57
+ "default": "us",
58
+ "description": "Default proxy country for stealth browser (us, gb, de, etc.)"
59
+ }
60
+ }
61
+ },
62
+ "credentialSource": {
63
+ "type": "string",
64
+ "enum": ["none", "auto", "keychain", "1password", "vault"],
65
+ "default": "none",
66
+ "description": "Where to look up login passwords for auto-login"
67
+ }
68
+ }
69
+ },
70
+ "uiHints": {
71
+ "skillsOutputDir": {
72
+ "label": "Skills Output Directory",
73
+ "help": "Where generated skills are saved locally",
74
+ "placeholder": "~/.openclaw/skills"
75
+ },
76
+ "browserPort": {
77
+ "label": "Browser Control Port",
78
+ "help": "Port for browser CDP control API (default: 18791)"
79
+ },
80
+ "autoDiscover": {
81
+ "label": "Auto-Discover Skills",
82
+ "help": "Automatically generate skills when browsing APIs"
83
+ },
84
+ "skillIndexUrl": {
85
+ "label": "Skill Marketplace URL",
86
+ "help": "Cloud skill marketplace API endpoint"
87
+ },
88
+ "marketplace.creatorWallet": {
89
+ "label": "Creator Wallet (Solana)",
90
+ "help": "Your Solana wallet address for receiving USDC when others download your skills",
91
+ "placeholder": "ABC123..."
92
+ },
93
+ "marketplace.solanaPrivateKey": {
94
+ "label": "Payment Private Key",
95
+ "help": "Base58-encoded Solana private key for paying x402 USDC when downloading paid skills",
96
+ "sensitive": true
97
+ },
98
+ "marketplace.defaultPrice": {
99
+ "label": "Default Skill Price",
100
+ "help": "Default price when publishing skills (0 = free)",
101
+ "placeholder": "0"
102
+ },
103
+ "browser.useApiKey": {
104
+ "label": "Browser Use API Key",
105
+ "help": "API key for stealth cloud browsers (browser-use.com)",
106
+ "sensitive": true,
107
+ "placeholder": "bu_..."
108
+ },
109
+ "browser.proxyCountry": {
110
+ "label": "Proxy Country",
111
+ "help": "Default proxy location for stealth browser",
112
+ "placeholder": "us"
113
+ },
114
+ "credentialSource": {
115
+ "label": "Credential Source",
116
+ "help": "Where to look up login passwords. 'keychain' = macOS Keychain, '1password' = 1Password CLI"
117
+ }
118
+ }
119
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@getfoundry/unbrowse-openclaw",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "description": "API reverse engineering for OpenClaw. Capture any API and turn it into monetizable skills for AI agents.",
8
+ "license": "UNLICENSED",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/lekt9/unbrowse-openclaw"
12
+ },
13
+ "keywords": [
14
+ "openclaw",
15
+ "clawdbot",
16
+ "ai-agent",
17
+ "api-reverse-engineering",
18
+ "browser-automation",
19
+ "skill-marketplace",
20
+ "x402",
21
+ "solana"
22
+ ],
23
+ "files": [
24
+ "dist/index.js",
25
+ "dist/index.d.ts",
26
+ "native/index.js",
27
+ "native/index.d.ts",
28
+ "native/unbrowse-native.darwin-arm64.node",
29
+ "native/unbrowse-native.darwin-x64.node",
30
+ "native/unbrowse-native.linux-x64-gnu.node",
31
+ "native/unbrowse-native.win32-x64-msvc.node",
32
+ "hooks/",
33
+ "openclaw.plugin.json",
34
+ "clawdbot.plugin.json"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "openclaw": {
41
+ "extensions": ["./dist/index.js"],
42
+ "hooks": ["./hooks/auto-discover"]
43
+ },
44
+ "clawdbot": {
45
+ "extensions": ["./dist/index.js"],
46
+ "hooks": ["./hooks/auto-discover"]
47
+ },
48
+ "moltbot": {
49
+ "extensions": ["./dist/index.js"],
50
+ "hooks": ["./hooks/auto-discover"]
51
+ },
52
+ "dependencies": {},
53
+ "peerDependencies": {
54
+ "openclaw": "*",
55
+ "clawdbot": "*",
56
+ "moltbot": "*"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "openclaw": { "optional": true },
60
+ "clawdbot": { "optional": true },
61
+ "moltbot": { "optional": true }
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^22.0.0",
65
+ "typescript": "^5.7.0"
66
+ }
67
+ }