webpipe-sdk 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b49c39a2dc09f4401f0cee5341c3aff38edf3faea72ca270e0ad3568918931a5
4
+ data.tar.gz: 7faf803b7c1193549d4c611b5b0cf42b51f0a2511c377d3379737a99a1ca0c90
5
+ SHA512:
6
+ metadata.gz: dca50f00ecced076a680314d2131122a52e9dd2e5a19982b8549d6486107a74d40e2c08db1deeb42c1743978ce415e596209a912f1a251b7536fd4a90d97c0c9
7
+ data.tar.gz: de0802c5500aa2a080ed86a97f17471b8187bfa3d5b4f188d19d77ab0b0ae017c54d822cff3e1685ca4b4707ebc7504a543c6a2135f797a6c30aae1ac01c6a41
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # webpipe-sdk (Ruby)
2
+
3
+ Official Ruby SDK for [WebPipe.ai](https://webpipe.ai/) — turn any webpage into clean, structured data.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ gem install webpipe-sdk
9
+ ```
10
+
11
+ Requires Ruby ≥ 3.0. **Zero runtime dependencies** — stdlib `net/http` only.
12
+
13
+ ## Quick Start
14
+
15
+ ```ruby
16
+ require "webpipe"
17
+
18
+ client = Webpipe::Client.new # reads WEBPIPE_API_KEY, or Webpipe::Client.new(api_key: "wc-...")
19
+
20
+ # Scrape a page
21
+ doc = client.scrape("https://example.com", formats: ["markdown", "metadata"])
22
+ puts doc.markdown
23
+ puts doc.metadata["title"]
24
+ ```
25
+
26
+ ### Crawl a whole site (polls automatically)
27
+
28
+ ```ruby
29
+ job = client.crawl("https://docs.example.com", limit: 50,
30
+ scrape_options: { formats: ["markdown"] }, timeout: 600)
31
+ job.data.each do |page|
32
+ puts "#{page.metadata["sourceURL"]} #{page.metadata["title"]}"
33
+ end
34
+ ```
35
+
36
+ Need control? `job = client.start_crawl(url, limit: 50)` returns immediately; poll with `client.get_crawl_status(job.id)`.
37
+
38
+ ### Browser session (JS-rendered / logged-in pages)
39
+
40
+ ```ruby
41
+ session = client.browser.create(url: "https://example.com/login")
42
+ begin
43
+ session.wait_until_running
44
+ session.fill("input[name=email]", "you@example.com")
45
+ session.fill("input[name=password]", "secret")
46
+ session.press("Enter", wait_after: 3000)
47
+
48
+ doc = session.navigate("https://example.com/dashboard", formats: ["markdown"])
49
+ puts doc.markdown
50
+ session.save_session # reuse login state next time via session_name
51
+ ensure
52
+ session.close
53
+ end
54
+ ```
55
+
56
+ ### Map (discover all URLs)
57
+
58
+ ```ruby
59
+ client.map("https://example.com", limit: 100, search: "blog").each do |link|
60
+ puts "#{link.url} #{link.title}"
61
+ end
62
+ ```
63
+
64
+ ## Errors
65
+
66
+ ```ruby
67
+ begin
68
+ client.scrape("https://example.com")
69
+ rescue Webpipe::AuthenticationError
70
+ # bad API key
71
+ rescue Webpipe::RateLimitError => e
72
+ # e.status_code == 429; e.message keeps the server's error text
73
+ rescue Webpipe::Error
74
+ # anything else
75
+ end
76
+ ```
77
+
78
+ `429` and `5xx` responses are retried automatically with exponential backoff (`max_retries: 2` by default).
79
+
80
+ ## Configuration
81
+
82
+ | Param | Env var | Default |
83
+ |---|---|---|
84
+ | `api_key` | `WEBPIPE_API_KEY` | — (required) |
85
+ | `base_url` | `WEBPIPE_API_URL` | `https://api.web2json.ai` |
86
+ | `timeout` | — | 60s |
87
+ | `max_retries` | — | 2 |
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ bundle install
93
+ ruby -Ilib -Itest test/client_test.rb
94
+ ruby -Ilib -Itest test/browser_test.rb
95
+ ```
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webpipe
4
+ # Factory for browser sessions, exposed as +client.browser+.
5
+ # @api private (constructed via Client)
6
+ class BrowserResource
7
+ def initialize(http)
8
+ @http = http
9
+ end
10
+
11
+ # Create a session. It starts asynchronously — call +wait_until_running+
12
+ # before sending commands for a controlled experience.
13
+ def create(url: nil, cookie: nil, proxy: nil, session_name: nil)
14
+ body = drop_nil(url: url, cookie: cookie, proxy: proxy, session_name: session_name)
15
+ info = @http.post("/api/v1/browser", body)
16
+ BrowserSession.new(@http, id: info["id"], status: info["status"], url: info["url"],
17
+ session_name: info["session_name"], expires_at: info["expires_at"])
18
+ end
19
+
20
+ private
21
+
22
+ def drop_nil(hash) = hash.reject { |_, v| v.nil? }
23
+ end
24
+
25
+ # Handle to a persistent headless Chromium session on the server.
26
+ #
27
+ # Server-side facts: sessions expire after 30 minutes idle; at most 2
28
+ # concurrent running sessions per user. Always +close+ when done.
29
+ class BrowserSession
30
+ attr_reader :id, :status, :url, :session_name, :expires_at
31
+
32
+ TERMINAL_STATUSES = %w[stopped error expired].freeze
33
+
34
+ def initialize(http, id:, status:, url: nil, session_name: nil, expires_at: nil)
35
+ @http = http
36
+ @id = id
37
+ @status = status
38
+ @url = url
39
+ @session_name = session_name
40
+ @expires_at = expires_at
41
+ end
42
+
43
+ # Fetch the latest session state from the server.
44
+ def refresh
45
+ info = @http.get(base)
46
+ @status = info["status"]
47
+ @url = info["url"]
48
+ @session_name = info["session_name"]
49
+ @expires_at = info["expires_at"]
50
+ self
51
+ end
52
+
53
+ # Block until the session is ready to accept commands.
54
+ def wait_until_running(timeout: 60, poll_interval: 1)
55
+ deadline = Time.now + timeout
56
+ loop do
57
+ refresh
58
+ return self if @status == "running"
59
+ raise Error, "Browser session #{@id} entered terminal status '#{@status}'" if TERMINAL_STATUSES.include?(@status)
60
+ raise PollTimeoutError, "Browser session #{@id} not running after #{timeout}s" if Time.now >= deadline
61
+
62
+ sleep poll_interval
63
+ end
64
+ end
65
+
66
+ # Stop the session and release server resources.
67
+ def close
68
+ @http.delete(base)
69
+ @status = "stopped"
70
+ nil
71
+ end
72
+
73
+ # Navigate to a new URL (with anti-bot handling) and scrape it.
74
+ def navigate(url, formats: nil, scroll_to_bottom: nil, max_scrolls: nil, scroll_wait: nil, scroll_step: nil)
75
+ body = { url: url }.merge(scrape_body(formats:, scroll_to_bottom:, max_scrolls:, scroll_wait:, scroll_step:))
76
+ Document.from_hash(@http.post("#{base}/navigate", body)["data"])
77
+ end
78
+
79
+ # Scrape the current page without navigating (faster than +navigate+).
80
+ def scrape(formats: nil, scroll_to_bottom: nil, max_scrolls: nil, scroll_wait: nil, scroll_step: nil)
81
+ body = scrape_body(formats:, scroll_to_bottom:, max_scrolls:, scroll_wait:, scroll_step:)
82
+ Document.from_hash(@http.post("#{base}/scrape", body)["data"])
83
+ end
84
+
85
+ # Run a raw browser action. Returns the raw API payload Hash.
86
+ def action(type, **params)
87
+ @http.post("#{base}/action", { type: type }.merge(params))
88
+ end
89
+
90
+ def click(selector, wait_after: nil) = action("click", **drop_nil(selector:, wait_after:))
91
+ def fill(selector, value) = action("fill", selector:, value:)
92
+ def select(selector, value) = action("select", selector:, value:)
93
+
94
+ def press(key, selector: nil, wait_after: nil)
95
+ action("press", **drop_nil(key:, selector:, wait_after:))
96
+ end
97
+
98
+ def scroll(direction: "down", amount: nil) = action("scroll", **drop_nil(direction:, amount:))
99
+ def wait(ms = 1000) = action("wait", ms:)
100
+ def wait_for(selector, timeout: nil) = action("wait_for", **drop_nil(selector:, timeout:))
101
+
102
+ # Take a screenshot. The API returns a base64 PNG inside the payload.
103
+ def screenshot(full_page: false) = action("screenshot", full_page:)
104
+
105
+ # Evaluate a JavaScript expression in the page (value in payload's "result").
106
+ def evaluate(expression) = action("evaluate", expression:)
107
+ def hover(selector) = action("hover", selector:)
108
+ def check(selector) = action("check", selector:)
109
+
110
+ def get_cookies
111
+ data = @http.get("#{base}/cookies")
112
+ data["cookies"] || data["data"] || []
113
+ end
114
+
115
+ def set_cookies(cookies) = @http.post("#{base}/cookies", { op: "set", cookies: cookies })
116
+ def clear_cookies = @http.post("#{base}/cookies", { op: "clear" })
117
+
118
+ # Persist cookies/storage under this session's +session_name+.
119
+ def save_session = @http.post("#{base}/save_session")
120
+
121
+ private
122
+
123
+ def base = "/api/v1/browser/#{@id}"
124
+
125
+ def scrape_body(formats:, scroll_to_bottom:, max_scrolls:, scroll_wait:, scroll_step:)
126
+ drop_nil(
127
+ formats: formats, scroll_to_bottom: scroll_to_bottom,
128
+ max_scrolls: max_scrolls, scroll_wait: scroll_wait, scroll_step: scroll_step
129
+ )
130
+ end
131
+
132
+ def drop_nil(hash) = hash.reject { |_, v| v.nil? }
133
+ end
134
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webpipe
4
+ # The WebPipe API client.
5
+ #
6
+ # client = Webpipe::Client.new # reads WEBPIPE_API_KEY
7
+ # doc = client.scrape("https://example.com", formats: ["markdown"])
8
+ # puts doc.markdown
9
+ class Client
10
+ DEFAULT_BASE_URL = "https://api.web2json.ai"
11
+
12
+ attr_reader :browser
13
+
14
+ def initialize(api_key: nil, base_url: nil, timeout: 60, max_retries: 2)
15
+ api_key ||= ENV["WEBPIPE_API_KEY"]
16
+ if api_key.nil? || api_key.empty?
17
+ raise ArgumentError,
18
+ "Missing API key. Pass api_key: or set the WEBPIPE_API_KEY environment " \
19
+ "variable. Create one at https://webpipe.ai/api-keys.html"
20
+ end
21
+
22
+ base_url ||= ENV["WEBPIPE_API_URL"] || DEFAULT_BASE_URL
23
+ @http = HttpClient.new(
24
+ base_url: base_url.sub(%r{/+$}, ""),
25
+ api_key: api_key,
26
+ timeout: timeout,
27
+ max_retries: max_retries,
28
+ user_agent: "webpipe-sdk-ruby/#{Webpipe::VERSION}"
29
+ )
30
+ @browser = BrowserResource.new(@http)
31
+ end
32
+
33
+ # Scrape a single page into markdown/html/links/metadata/json.
34
+ def scrape(url, formats: nil, json_schema: nil, use_proxy: nil, proxy: nil, cookie: nil)
35
+ body = drop_nil(url:, formats:, json_schema:, use_proxy:, proxy:, cookie:)
36
+ Document.from_hash(@http.post("/api/v1/scrape", body)["data"])
37
+ end
38
+
39
+ # Discover URLs of a website (robots.txt → sitemap → in-page links).
40
+ def map(url, limit: nil, search: nil, sitemap: nil, same_domain: nil,
41
+ use_proxy: nil, proxy: nil, cookie: nil)
42
+ body = drop_nil(url:, limit:, search:, sitemap:, same_domain:, use_proxy:, proxy:, cookie:)
43
+ (@http.post("/api/v1/map", body)["links"] || []).map { |l| MapLink.from_hash(l) }
44
+ end
45
+
46
+ # Submit an async crawl job and return its handle immediately.
47
+ def start_crawl(url, limit: nil, max_discovery_depth: nil, include_paths: nil,
48
+ exclude_paths: nil, allow_subdomains: nil, allow_external_links: nil,
49
+ crawl_entire_domain: nil, ignore_query_params: nil, sitemap: nil,
50
+ delay: nil, max_concurrency: nil, scrape_options: nil,
51
+ use_proxy: nil, proxy: nil, cookie: nil)
52
+ body = drop_nil(
53
+ url:, limit:, max_discovery_depth:, include_paths:, exclude_paths:,
54
+ allow_subdomains:, allow_external_links:, crawl_entire_domain:,
55
+ ignore_query_params:, sitemap:, delay:, max_concurrency:,
56
+ scrape_options:, use_proxy:, proxy:, cookie:
57
+ )
58
+ res = @http.post("/api/v1/crawl", body)
59
+ CrawlJobStart.new(res.fetch("id"), res["url"])
60
+ end
61
+
62
+ # Fetch the current status and partial results of a crawl job.
63
+ def get_crawl_status(job_id)
64
+ CrawlJob.from_hash(@http.get("/api/v1/crawl/#{job_id}"))
65
+ end
66
+
67
+ # Submit a crawl job and poll until it completes.
68
+ #
69
+ # Yields the current CrawlJob after each poll when a block is given —
70
+ # the idiomatic Ruby way to observe a long crawl (progress bars, logging).
71
+ #
72
+ # Raises CrawlError if the job fails, PollTimeoutError if +timeout+ is
73
+ # exceeded. Other keywords are the same as +start_crawl+.
74
+ def crawl(url, poll_interval: 2, timeout: nil, **options)
75
+ job = start_crawl(url, **options)
76
+ deadline = timeout && Time.now + timeout
77
+ loop do
78
+ status = get_crawl_status(job.id)
79
+ return status if status.status == CrawlJob::STATUS_COMPLETED
80
+ raise CrawlError, "Crawl job #{job.id} failed: #{status.errors}" if status.status == CrawlJob::STATUS_FAILED
81
+ yield status if block_given?
82
+ if deadline && Time.now >= deadline
83
+ raise PollTimeoutError, "Crawl job #{job.id} did not complete within #{timeout}s"
84
+ end
85
+
86
+ sleep poll_interval
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def drop_nil(hash) = hash.reject { |_, v| v.nil? }
93
+ end
94
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webpipe
4
+ # Base class for all webpipe-sdk errors.
5
+ class Error < StandardError; end
6
+
7
+ # An error response returned by the WebPipe API. Always carries the server's
8
+ # original error message and the HTTP status code.
9
+ class ApiError < Error
10
+ attr_reader :status_code, :body
11
+
12
+ def initialize(message, status_code:, body: nil)
13
+ super("[#{status_code}] #{message}")
14
+ @status_code = status_code
15
+ @body = body
16
+ end
17
+ end
18
+
19
+ # 400 — invalid parameters (missing url, bad format, ...).
20
+ class BadRequestError < ApiError; end
21
+ # 401 — API key missing, invalid or deleted.
22
+ class AuthenticationError < ApiError; end
23
+ # 403 — accessing a resource owned by another user.
24
+ class ForbiddenError < ApiError; end
25
+ # 404 — job/session does not exist or has expired.
26
+ class NotFoundError < ApiError; end
27
+ # 429 — rate limit exceeded, or too many concurrent browser sessions.
28
+ class RateLimitError < ApiError; end
29
+ # 5xx — scrape timeout, target site unreachable, ...
30
+ class ServerError < ApiError; end
31
+
32
+ # A crawl job reached status "failed".
33
+ class CrawlError < Error; end
34
+
35
+ # Polling did not finish within the allotted timeout.
36
+ class PollTimeoutError < Error; end
37
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Webpipe
8
+ # Thin net/http wrapper: auth headers, retries with backoff, error mapping.
9
+ # @api private
10
+ class HttpClient
11
+ RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
12
+ RETRYABLE_ERRORS = [
13
+ Net::OpenTimeout, Net::ReadTimeout, SocketError, SystemCallError
14
+ ].freeze
15
+
16
+ STATUS_TO_ERROR = {
17
+ 400 => BadRequestError,
18
+ 401 => AuthenticationError,
19
+ 403 => ForbiddenError,
20
+ 404 => NotFoundError,
21
+ 429 => RateLimitError
22
+ }.freeze
23
+
24
+ def initialize(base_url:, api_key:, timeout:, max_retries:, user_agent:)
25
+ @base_uri = URI(base_url)
26
+ @api_key = api_key
27
+ @timeout = timeout
28
+ @max_retries = max_retries
29
+ @user_agent = user_agent
30
+ end
31
+
32
+ def get(path) = request("GET", path)
33
+ def post(path, body = nil) = request("POST", path, body)
34
+ def delete(path) = request("DELETE", path)
35
+
36
+ def request(method, path, body = nil)
37
+ attempt = 0
38
+ loop do
39
+ response = perform(method, path, body)
40
+ if RETRYABLE_STATUSES.include?(response.code.to_i) && attempt < @max_retries
41
+ attempt += 1
42
+ sleep backoff(response, attempt)
43
+ next
44
+ end
45
+ return handle(response)
46
+ rescue *RETRYABLE_ERRORS
47
+ attempt += 1
48
+ raise if attempt > @max_retries
49
+
50
+ sleep(0.5 * (2**attempt))
51
+ retry
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def perform(method, path, body)
58
+ http = Net::HTTP.new(@base_uri.host, @base_uri.port)
59
+ http.use_ssl = @base_uri.scheme == "https"
60
+ http.open_timeout = @timeout
61
+ http.read_timeout = @timeout
62
+
63
+ request_class = Net::HTTP.const_get(method.capitalize)
64
+ request = request_class.new(path)
65
+ request["Authorization"] = "Bearer #{@api_key}"
66
+ request["Content-Type"] = "application/json"
67
+ request["User-Agent"] = @user_agent
68
+ request.body = JSON.generate(body) if body
69
+
70
+ http.request(request)
71
+ end
72
+
73
+ def handle(response)
74
+ status = response.code.to_i
75
+ payload = begin
76
+ body = response.body.to_s
77
+ body.empty? ? {} : JSON.parse(body)
78
+ rescue JSON::ParserError
79
+ {}
80
+ end
81
+
82
+ return payload if status < 400
83
+
84
+ message = payload.is_a?(Hash) && payload["error"] ? payload["error"] : response.message
85
+ error_class = status >= 500 ? ServerError : (STATUS_TO_ERROR[status] || ApiError)
86
+ raise error_class.new(message, status_code: status, body: payload)
87
+ end
88
+
89
+ def backoff(response, attempt)
90
+ retry_after = response["Retry-After"]
91
+ return Float(retry_after) if retry_after
92
+
93
+ 0.5 * (2**attempt)
94
+ rescue ArgumentError, TypeError
95
+ 0.5 * (2**attempt)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webpipe
4
+ # A link extracted from a page.
5
+ Link = Struct.new(:text, :href) do
6
+ def self.from_hash(hash)
7
+ new(hash["text"], hash["href"])
8
+ end
9
+ end
10
+
11
+ # Result of scraping a single page (scrape / browser / crawl data item).
12
+ # `metadata` is a plain Hash — the API adds extra keys (og:*, sourceURL, ...).
13
+ class Document
14
+ attr_reader :markdown, :html, :raw_html, :links, :metadata, :json
15
+
16
+ def initialize(markdown: nil, html: nil, raw_html: nil, links: nil, metadata: nil, json: nil)
17
+ @markdown = markdown
18
+ @html = html
19
+ @raw_html = raw_html
20
+ @links = links
21
+ @metadata = metadata
22
+ @json = json
23
+ end
24
+
25
+ def self.from_hash(hash)
26
+ hash ||= {}
27
+ new(
28
+ markdown: hash["markdown"],
29
+ html: hash["html"],
30
+ raw_html: hash["rawHtml"],
31
+ links: hash["links"]&.map { |l| Link.from_hash(l) },
32
+ metadata: hash["metadata"],
33
+ json: hash["json"]
34
+ )
35
+ end
36
+ end
37
+
38
+ # A URL discovered by the Map endpoint.
39
+ MapLink = Struct.new(:url, :title, :description) do
40
+ def self.from_hash(hash)
41
+ new(hash["url"], hash["title"], hash["description"])
42
+ end
43
+ end
44
+
45
+ # Handle returned when a crawl job is submitted.
46
+ CrawlJobStart = Struct.new(:id, :url)
47
+
48
+ # Status and (partial or final) results of a crawl job.
49
+ # Results expire 24h after completion (see +expires_at+).
50
+ class CrawlJob
51
+ STATUS_SCRAPING = "scraping"
52
+ STATUS_COMPLETED = "completed"
53
+ STATUS_FAILED = "failed"
54
+
55
+ attr_reader :status, :total, :completed, :data, :errors, :expires_at
56
+
57
+ def initialize(status:, total: 0, completed: 0, data: [], errors: [], expires_at: nil)
58
+ @status = status
59
+ @total = total
60
+ @completed = completed
61
+ @data = data
62
+ @errors = errors
63
+ @expires_at = expires_at
64
+ end
65
+
66
+ def self.from_hash(hash)
67
+ new(
68
+ status: hash["status"],
69
+ total: hash["total"] || 0,
70
+ completed: hash["completed"] || 0,
71
+ data: (hash["data"] || []).map { |d| Document.from_hash(d) },
72
+ errors: hash["errors"] || [],
73
+ expires_at: hash["expires_at"]
74
+ )
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webpipe
4
+ VERSION = "0.1.0"
5
+ end
data/lib/webpipe.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # webpipe-sdk — the official Ruby SDK for WebPipe.ai.
4
+ #
5
+ # require "webpipe"
6
+ # client = Webpipe::Client.new(api_key: "wc-...")
7
+ # doc = client.scrape("https://example.com", formats: ["markdown"])
8
+ # puts doc.markdown
9
+
10
+ require_relative "webpipe/version"
11
+ require_relative "webpipe/errors"
12
+ require_relative "webpipe/types"
13
+ require_relative "webpipe/http_client"
14
+ require_relative "webpipe/browser"
15
+ require_relative "webpipe/client"
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: webpipe-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - WebPipe.ai
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Scrape, Map, Crawl and persistent Browser Sessions via the WebPipe.ai
13
+ API. Zero runtime dependencies (stdlib net/http).
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - README.md
19
+ - lib/webpipe.rb
20
+ - lib/webpipe/browser.rb
21
+ - lib/webpipe/client.rb
22
+ - lib/webpipe/errors.rb
23
+ - lib/webpipe/http_client.rb
24
+ - lib/webpipe/types.rb
25
+ - lib/webpipe/version.rb
26
+ homepage: https://webpipe.ai
27
+ licenses:
28
+ - MIT
29
+ metadata: {}
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '3.0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 4.0.16
45
+ specification_version: 4
46
+ summary: Official Ruby SDK for WebPipe.ai — turn any webpage into clean, structured
47
+ data
48
+ test_files: []