surfbird 0.0.0-stage → 0.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.
Files changed (3) hide show
  1. package/README.md +54 -2
  2. package/index.mjs +103 -0
  3. package/package.json +35 -4
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # surfbird
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Zero-dependency JS/TS client for **[Surfbird](https://github.com/terry2010/surfbird)** — the unofficial self-hosted Docker distribution of [Cloudflare Kitesurf](https://github.com/cloudflare/kitesurf), a headless browser engine built for AI agents.
4
+
5
+ Turn any VPS into a browser API: article→markdown extraction (Readability+turndown), rendered DOM, PNG screenshots, PDFs, page-context JS evaluation, and sequential batch fetching — over 6 tiny REST endpoints.
6
+
7
+ ```bash
8
+ npm install surfbird
9
+ ```
10
+
11
+ ```js
12
+ import { Surfbird } from "surfbird";
13
+
14
+ const sb = new Surfbird("http://vps:8080", { token: process.env.SURFBIRD_TOKEN });
15
+
16
+ // article → clean markdown (the RSS-digest / LLM-summarizer path)
17
+ const { title, markdown } = await sb.markdown("https://blog.example.com/post");
18
+
19
+ // rendered DOM / binary output
20
+ const { html } = await sb.content("https://spa.example.com");
21
+ const png = await sb.screenshot(url, { fullPage: true });
22
+ const pdf = await sb.pdf(url);
23
+
24
+ // run JS inside the page
25
+ const links = await sb.evaluate(url, `[...document.querySelectorAll("a")].map(a=>a.href)`);
26
+
27
+ // many urls, one call — sequential server-side, failures don't abort the batch
28
+ const results = await sb.batch("markdown", [url1, url2, url3]);
29
+ // → [{url, ok:true, status:200, data:{...}}, {url, ok:false, status:422, error:"no_article"}]
30
+ ```
31
+
32
+ ## API
33
+
34
+ | Method | Returns |
35
+ |---|---|
36
+ | `sb.markdown(url)` | `{title, byline, excerpt, url, markdown}` (throws `SurfbirdError` status 422 when no article) |
37
+ | `sb.content(url)` | `{title, url, html}` |
38
+ | `sb.screenshot(url, {width, height, fullPage})` | PNG `Buffer` |
39
+ | `sb.pdf(url, {landscape, format})` | PDF `Buffer` |
40
+ | `sb.evaluate(url, script)` | JSON-serializable result |
41
+ | `sb.batch(action, urls, {timeout})` | `[{url, ok, status, data\|error}]` (action: markdown/content/evaluate/screenshot/pdf) |
42
+ | `sb.health()` | `boolean` |
43
+ | `sb.openapi()` | server's OpenAPI JSON |
44
+
45
+ Env vars used by examples: `SURFBIRD_URL`, `SURFBIRD_TOKEN`.
46
+
47
+ ## Get a server
48
+
49
+ ```bash
50
+ docker run -d -p 8080:8080 -e SURFBIRD_TOKEN=$(openssl rand -hex 24) terry2010/surfbird:auth
51
+ ```
52
+
53
+ MCP instead of a client lib? → [`surfbird-mcp`](https://www.npmjs.com/package/surfbird-mcp)
54
+
55
+ Apache-2.0 · Not affiliated with Cloudflare · [GitHub](https://github.com/terry2010/surfbird)
package/index.mjs ADDED
@@ -0,0 +1,103 @@
1
+ // surfbird — JS/TS client for the Surfbird REST API. Zero dependencies.
2
+ //
3
+ // import { Surfbird } from "surfbird";
4
+ // const sb = new Surfbird("http://vps:8080", { token: "..." });
5
+ // const { markdown } = await sb.markdown("https://example.com/post");
6
+
7
+ export class SurfbirdError extends Error {
8
+ constructor(status, code, message) {
9
+ super(`HTTP ${status} ${code}: ${message}`);
10
+ this.status = status;
11
+ this.code = code;
12
+ }
13
+ }
14
+
15
+ export class Surfbird {
16
+ /**
17
+ * @param {string} base e.g. "http://127.0.0.1:8080" (auth) or ":8787" (raw)
18
+ * @param {object} [opts]
19
+ * @param {string} [opts.token] Bearer token (required on auth images)
20
+ * @param {number} [opts.timeout] per-request ms, default 120000
21
+ */
22
+ constructor(base = "http://127.0.0.1:8080", { token = "", timeout = 120_000 } = {}) {
23
+ this.base = base.replace(/\/+$/, "");
24
+ this.token = token;
25
+ this.timeout = timeout;
26
+ }
27
+
28
+ async #post(action, body, timeout = this.timeout) {
29
+ const res = await fetch(`${this.base}/${action}`, {
30
+ method: "POST",
31
+ headers: {
32
+ "content-type": "application/json",
33
+ ...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
34
+ },
35
+ body: JSON.stringify(body),
36
+ signal: AbortSignal.timeout(timeout),
37
+ });
38
+ const buf = Buffer.from(await res.arrayBuffer());
39
+ if (!res.ok) {
40
+ let code = "http_error", message = res.statusText;
41
+ try {
42
+ const e = JSON.parse(buf.toString()).error || {};
43
+ if (e.code) code = e.code;
44
+ if (e.message) message = e.message;
45
+ } catch { /* non-JSON error body */ }
46
+ throw new SurfbirdError(res.status, code, message);
47
+ }
48
+ return buf;
49
+ }
50
+
51
+ /** Engine liveness (unauthenticated). */
52
+ async health() {
53
+ try {
54
+ const r = await fetch(`${this.base}/health`, { signal: AbortSignal.timeout(10_000) });
55
+ return r.ok;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /** OpenAPI description of the server's REST surface (unauthenticated). */
62
+ async openapi() {
63
+ const r = await fetch(`${this.base}/openapi.json`, { signal: AbortSignal.timeout(10_000) });
64
+ return r.json();
65
+ }
66
+
67
+ /** Article extraction → {title, byline, excerpt, url, markdown}. Throws 422 SurfbirdError when no article. */
68
+ async markdown(url, opts = {}) {
69
+ return JSON.parse((await this.#post("markdown", { url, ...opts })).toString());
70
+ }
71
+
72
+ /** Rendered DOM → {title, url, html}. */
73
+ async content(url, opts = {}) {
74
+ return JSON.parse((await this.#post("content", { url, ...opts })).toString());
75
+ }
76
+
77
+ /** PNG screenshot → Buffer. opts: {width, height, fullPage} */
78
+ async screenshot(url, opts = {}) {
79
+ return this.#post("screenshot", { url, ...opts });
80
+ }
81
+
82
+ /** PDF → Buffer. opts: {landscape, format} */
83
+ async pdf(url, opts = {}) {
84
+ return this.#post("pdf", { url, ...opts });
85
+ }
86
+
87
+ /** Evaluate JS in page context → result (JSON-serializable). */
88
+ async evaluate(url, script, opts = {}) {
89
+ return (JSON.parse((await this.#post("evaluate", { url, script, ...opts })).toString())).result;
90
+ }
91
+
92
+ /**
93
+ * Run one action over many urls, sequentially (respects the server
94
+ * concurrency gate). Returns [{url, ok, status, data|error}] in input order.
95
+ * opts: {timeout} — overall batch timeout, default 10min.
96
+ */
97
+ async batch(action, urls, opts = {}) {
98
+ const res = await this.#post("batch", { action, urls }, opts.timeout ?? 600_000);
99
+ return JSON.parse(res.toString());
100
+ }
101
+ }
102
+
103
+ export default Surfbird;
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "surfbird",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "JS/TS client for Surfbird — the self-hosted Kitesurf headless browser engine. Article markdown extraction, screenshots, PDFs, page JS evaluation and batch fetching over a tiny REST API.",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "files": [
11
+ "index.mjs",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "surfbird",
16
+ "kitesurf",
17
+ "headless-browser",
18
+ "web-scraping",
19
+ "markdown",
20
+ "readability",
21
+ "screenshot",
22
+ "pdf",
23
+ "ai-agents",
24
+ "browser-automation",
25
+ "self-hosted"
26
+ ],
27
+ "license": "Apache-2.0",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/terry2010/surfbird.git",
31
+ "directory": "sdk"
32
+ },
33
+ "homepage": "https://github.com/terry2010/surfbird",
34
+ "engines": {
35
+ "node": ">=18"
36
+ }
37
+ }