meerkat-agents 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: 0270f21d356660800f70b0e6e47a6c47bbd8978533748d150e9e7145d3ed8dbb
4
+ data.tar.gz: 37afac9bf07c8bc93bb51e43a31fd2968c09742ccfbac506df3cae5a1f00eeb7
5
+ SHA512:
6
+ metadata.gz: 6dda18ade21f2a9d99b49ebb30a64dbd4610012d40362f78469bbb2e9b269982f8b333a64068537e0c4321f5bcc79cc8133e13b9091bffbec4bfed7359854d5d
7
+ data.tar.gz: 85640b0912fb3730bbbb24541c60b8aa4be13877250cc018661e968e68d8f12f59e0d1606383ab4fcb3f27a18d6cb75377910f7f9efe1e8d88254755c7446c69
Binary file
Binary file
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-06-27
9
+
10
+ ### Added
11
+
12
+ - Initial release of the Meerkat Ruby client (`meerkat-agents` on RubyGems, `require "meerkat"`)
13
+ - Task, signup, and API key resources
14
+ - Webhook HMAC-SHA256 verification helpers
15
+ - Optional Rails railtie and webhook verification concern
16
+
17
+ [0.1.0]: https://github.com/Tiny-Bubble-Company/meerkat-ruby/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tiny Bubble Company
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,531 @@
1
+ <p align="center">
2
+ <a href="https://meerkatagents.com">
3
+ <img src=".github/readme-banner.png" alt="Meerkat — open source webhook-native agent task API" width="100%">
4
+ </a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <img src=".github/meerkat-logo.png" alt="Meerkat mascot" width="88" height="88">
9
+ <br>
10
+ <em>Describe a task in plain English. Meerkat runs it async — results POST to your webhook.</em>
11
+ </p>
12
+
13
+ # meerkat-agents
14
+
15
+ Official Ruby client for [Meerkat](https://github.com/Tiny-Bubble-Company/meerkat) — an open source, webhook-native API for async agent tasks.
16
+
17
+ Install as **`meerkat-agents`**, require as **`meerkat`**:
18
+
19
+ ```ruby
20
+ gem "meerkat-agents"
21
+ # ...
22
+ require "meerkat"
23
+ ```
24
+
25
+ [![Gem Version](https://badge.fury.io/rb/meerkat-agents.svg)](https://badge.fury.io/rb/meerkat-agents)
26
+ [![License: MIT](https://img.shields.io/badge/License-MIT-111111.svg)](LICENSE)
27
+
28
+ ---
29
+
30
+ ## About Meerkat
31
+
32
+ **Meerkat** is open source infrastructure for async agent tasks. You describe work in plain English, connect your own LLM key (BYOK), and Meerkat schedules execution and POSTs signed JSON to your webhook — without building schedulers, scrapers, or retry logic.
33
+
34
+ | Resource | Link |
35
+ |----------|------|
36
+ | **Website** | [meerkatagents.com](https://meerkatagents.com) |
37
+ | **Documentation** | [meerkatagents.com/docs](https://meerkatagents.com/docs) |
38
+ | **Use cases** | [Package tracking](https://meerkatagents.com/use-cases/package-tracking) · [Site monitoring](https://meerkatagents.com/use-cases/website-monitoring) · [Agent webhooks](https://meerkatagents.com/use-cases/agent-webhooks) |
39
+ | **Developer guides** | [meerkatagents.com/blog](https://meerkatagents.com/blog) |
40
+ | **Sign up (Cloud)** | [cloud.meerkatagents.com/signup](https://cloud.meerkatagents.com/signup) |
41
+ | **Open source server** | [github.com/Tiny-Bubble-Company/meerkat](https://github.com/Tiny-Bubble-Company/meerkat) |
42
+
43
+ **This repo** is the official **Ruby gem** (`meerkat-agents` on RubyGems). For product overview, use cases, and self-host vs Cloud, read more on the [Meerkat website](https://meerkatagents.com).
44
+
45
+ **Other SDKs:** [meerkat-python](https://github.com/Tiny-Bubble-Company/meerkat-python) · [meerkat-javascript](https://github.com/Tiny-Bubble-Company/meerkat-javascript)
46
+
47
+ ---
48
+
49
+ ## Table of contents
50
+
51
+ - [About Meerkat](#about-meerkat)
52
+ - [What is Meerkat?](#what-is-meerkat)
53
+ - [What problem does it solve?](#what-problem-does-it-solve)
54
+ - [Why use this gem?](#why-use-this-gem)
55
+ - [How it works](#how-it-works)
56
+ - [Use cases](#use-cases)
57
+ - [Task types](#task-types)
58
+ - [Self-host vs Meerkat Cloud](#self-host-vs-meerkat-cloud)
59
+ - [Installation](#installation)
60
+ - [Quick start](#quick-start)
61
+ - [Examples](#examples)
62
+ - [Receiving webhooks (Rails)](#receiving-webhooks-rails)
63
+ - [Output formats](#output-formats)
64
+ - [API reference](#api-reference)
65
+ - [Configuration](#configuration)
66
+ - [Development](#development)
67
+ - [License](#license)
68
+
69
+ ---
70
+
71
+ ## What is Meerkat?
72
+
73
+ Meerkat gives developers a single primitive for agentic async work:
74
+
75
+ ```
76
+ Register a task → Meerkat runs it → findings POST to your webhook
77
+ ```
78
+
79
+ Describe a task in plain English, pass structured `input_params`, bring your own LLM key (BYOK), and Meerkat's agent executes the work — fetching webpages, extracting structured findings, and delivering JSON to your `output_webhook` on a schedule or on demand.
80
+
81
+ **No polling. No per-carrier SDKs. No LLM tool loops to maintain.**
82
+
83
+ This gem wraps the Meerkat REST API so you can create tasks, trigger runs, and verify inbound webhooks from Ruby and Rails apps without hand-rolling HTTP clients.
84
+
85
+ ---
86
+
87
+ ## What problem does it solve?
88
+
89
+ Building async agent workflows usually means stitching together:
90
+
91
+ - A job queue and scheduler
92
+ - LLM calls with tool use (fetch URL, parse HTML, compare state)
93
+ - Webhook delivery and retry logic
94
+ - Change detection between runs
95
+
96
+ Meerkat handles all of that as a managed API. **meerkat-agents** handles the client side:
97
+
98
+ | Without the gem | With meerkat-agents |
99
+ |-----------------|-------------------|
100
+ | Raw `curl` / Faraday calls | Typed resource methods (`client.tasks.create`, `client.tasks.run`) |
101
+ | Manual auth headers | API key configured once |
102
+ | DIY webhook HMAC verification | `Meerkat::Webhooks.verify` + Rails concern |
103
+ | Error parsing by hand | `Meerkat::NotFoundError`, `ValidationError`, etc. |
104
+
105
+ ---
106
+
107
+ ## Why use this gem?
108
+
109
+ - **Webhook-native** — every task delivers JSON to your endpoint; the gem helps you verify signatures
110
+ - **BYOK** — Anthropic, OpenAI, OpenRouter, or Grok; keys stored on your Meerkat account
111
+ - **Recurring + one-off** — natural-language schedules (`every 2 hours`) or ad-hoc runs
112
+ - **Self-host or Cloud** — same API against Docker, Render, Fly.io, or [Meerkat Cloud](https://cloud.meerkatagents.com/signup)
113
+ - **Rails-ready** — optional railtie, env-based config, webhook verification concern
114
+
115
+ ---
116
+
117
+ ## How it works
118
+
119
+ ```
120
+ ┌─────────────┐ client.tasks.create ┌─────────────┐ agent + tools ┌─────────────┐
121
+ │ Your app │ ───────────────────────► │ Meerkat │ ──────────────────► │ Web / APIs │
122
+ └─────────────┘ └─────────────┘ └─────────────┘
123
+ ▲ │
124
+ │ POST output_webhook │
125
+ └──────────────────────────────────────────┘
126
+ (verify with Meerkat::Webhooks)
127
+ ```
128
+
129
+ | Step | What happens |
130
+ |------|--------------|
131
+ | **1. Register** | `client.tasks.create(...)` — describe work, pass params, set webhook |
132
+ | **2. Run** | Meerkat schedules recurring tasks or runs one-offs on demand |
133
+ | **3. Receive** | Findings POSTed to your webhook as structured JSON |
134
+
135
+ ---
136
+
137
+ ## Use cases
138
+
139
+ What teams ship with Meerkat — aligned with the [use cases on meerkatagents.com](https://meerkatagents.com):
140
+
141
+ | Use case | In one line | On the website |
142
+ |----------|-------------|----------------|
143
+ | **Package tracking** | Webhook when DHL, UPS, FedEx, or any carrier status changes | [Package tracking →](https://meerkatagents.com/use-cases/package-tracking) |
144
+ | **Website monitoring** | Watch a URL; get notified when the signal you describe happens | [Site monitoring →](https://meerkatagents.com/use-cases/website-monitoring) |
145
+ | **Price & stock** | Competitor price drops and restocks without building a scraper | [Guide →](https://meerkatagents.com/blog/competitor-price-stock-monitoring) |
146
+ | **Async agent webhooks** | Register LLM agent work; results POST to your endpoint | [Agent webhooks →](https://meerkatagents.com/use-cases/agent-webhooks) |
147
+
148
+ Read the full [developer guides](https://meerkatagents.com/blog) for step-by-step tutorials with API examples.
149
+
150
+ ---
151
+
152
+ ### Package tracking — webhook when status changes
153
+
154
+ Monitor any courier tracking link — DHL, UPS, FedEx, USPS, DPD, Royal Mail — with **one API**. Meerkat's agent reads the carrier page on a schedule, detects state changes, and POSTs structured JSON to your endpoint. No per-carrier SDKs, no polling loops, no LLM tool code to maintain.
155
+
156
+ - **Every carrier, one endpoint** — pass a `courier_tracking_link`; adding a new carrier is zero integration code.
157
+ - **Webhook on change only** — hear from Meerkat when status moves (*In transit → Out for delivery → Delivered*), not on every poll.
158
+ - **Signed and retried** — HMAC-SHA256 signatures, exponential backoff, delivery history via the events API.
159
+
160
+ ```ruby
161
+ client.tasks.create(
162
+ task_type: "recurring",
163
+ description: "Monitor DHL tracking and report status changes",
164
+ input_params: { courier_tracking_link: "https://www.dhl.de/track?id=..." },
165
+ frequency: "every 2 hours",
166
+ output_webhook: "https://your-app.com/webhooks/meerkat"
167
+ )
168
+ ```
169
+
170
+ → [Package tracking use case](https://meerkatagents.com/use-cases/package-tracking) · [Shipment tracking guide](https://meerkatagents.com/blog/shipment-tracking-api-webhook)
171
+
172
+ ---
173
+
174
+ ### Website monitoring — watch any URL
175
+
176
+ A change-detection API that understands **intent**, not just pixel diffs. Describe the signal in plain English — *price under $99*, *plan name changed*, *status page updated* — and Meerkat watches on a schedule. It only fires your webhook when that condition is met.
177
+
178
+ - **Semantic, not cosmetic diff** — no CSS selectors to maintain when the page layout shifts.
179
+ - **JS-rendered pages** — SPAs and dynamic content via headless fetch before reasoning.
180
+ - **Your schedule** — natural language (`every 15 minutes`), cron, or on-demand `client.tasks.run(id)`.
181
+
182
+ ```ruby
183
+ client.tasks.create(
184
+ task_type: "recurring",
185
+ description: "Notify me when this product drops below $99",
186
+ input_params: { url: "https://shop.example.com/widget-pro" },
187
+ frequency: "every 30 minutes",
188
+ output_webhook: "https://your-app.com/webhooks/meerkat"
189
+ )
190
+ ```
191
+
192
+ → [Website monitoring use case](https://meerkatagents.com/use-cases/website-monitoring) · [Monitor website for changes guide](https://meerkatagents.com/blog/monitor-website-for-changes)
193
+
194
+ ---
195
+
196
+ ### Price & stock monitoring — competitor alerts
197
+
198
+ Track competitor product pages and get a signed webhook when **price drops**, **price increases**, or **stock flips** (out of stock → in stock). One task per SKU — no scraper fleet, scheduler, or retry layer to operate.
199
+
200
+ ```ruby
201
+ client.tasks.create(
202
+ task_type: "recurring",
203
+ description: "Report price and stock whenever either changes",
204
+ input_params: { url: "https://competitor.com/product/sku-123" },
205
+ frequency: "every 15 minutes",
206
+ output_webhook: "https://your-app.com/webhooks/meerkat"
207
+ )
208
+ ```
209
+
210
+ Example webhook when price moves:
211
+
212
+ ```json
213
+ {
214
+ "data": {
215
+ "summary": "Price dropped from 99.00 to 89.00",
216
+ "findings": { "price": 89.0, "previous_price": 99.0, "in_stock": true },
217
+ "change_detected": true
218
+ }
219
+ }
220
+ ```
221
+
222
+ → [Competitor price & stock guide](https://meerkatagents.com/blog/competitor-price-stock-monitoring)
223
+
224
+ ---
225
+
226
+ ### Async agent webhooks — skip the scheduler boilerplate
227
+
228
+ Meerkat is the open source primitive for engineers who would rather ship product than rebuild **schedulers, LLM tool loops, retries, and signed webhook delivery**. Register a task in plain English, attach your LLM key (BYOK), point at an endpoint — done.
229
+
230
+ - **One verb: register** — `POST /tasks` creates recurring or one-off agent work.
231
+ - **Webhook-native results** — structured JSON, signed, retried until 2xx (same contract as Stripe or GitHub webhooks).
232
+ - **MIT & self-hostable** — Docker, Render, Fly.io, or Meerkat Cloud; identical REST API.
233
+
234
+ ```ruby
235
+ client.tasks.create(
236
+ task_type: "recurring",
237
+ description: "Summarize new arXiv papers in distributed systems",
238
+ input_params: { topic: "distributed systems" },
239
+ frequency: "0 9 * * *",
240
+ output_webhook: "https://your-app.com/webhooks/meerkat"
241
+ )
242
+ ```
243
+
244
+ → [Agent webhooks use case](https://meerkatagents.com/use-cases/agent-webhooks) · [Webhook vs polling guide](https://meerkatagents.com/blog/webhook-vs-polling)
245
+
246
+ ---
247
+
248
+ ### One-off lookups
249
+
250
+ Ad-hoc agent tasks without standing up a schedule — research, single URL extraction, or on-demand runs triggered from your app:
251
+
252
+ ```ruby
253
+ client.tasks.create(
254
+ task_type: "one_off",
255
+ description: "Fetch the current price of this product once",
256
+ input_params: { url: "https://example.com/product/123" },
257
+ output_webhook: "https://your-app.com/webhooks/meerkat"
258
+ )
259
+ ```
260
+
261
+ ---
262
+
263
+ ### Scheduled scraping with webhooks
264
+
265
+ Need data on a cadence, not just change detection? Set a frequency and receive structured findings every run — see the [web scraping API guide](https://meerkatagents.com/blog/web-scraping-api-scheduled-webhooks).
266
+
267
+ ---
268
+
269
+ ## Task types
270
+
271
+ | Type | Behavior | Best for |
272
+ |------|----------|----------|
273
+ | `recurring` | Runs on a schedule; compares state between runs; reports changes | Courier tracking, uptime, price watches |
274
+ | `one_off` | Runs once, reports findings, marks task complete | Ad-hoc research, single lookups |
275
+
276
+ **Frequency** accepts natural language (`every 30 minutes`, `hourly`, `every 2 hours`) or cron expressions. Required for `recurring` tasks; omit for `one_off`.
277
+
278
+ ---
279
+
280
+ ## Self-host vs Meerkat Cloud
281
+
282
+ | | Self-host (OSS) | Meerkat Cloud |
283
+ |--|-----------------|---------------|
284
+ | **API** | Identical | Identical |
285
+ | **LLM keys** | You supply (BYOK) | You supply (BYOK) |
286
+ | **Infra / ops** | You run Postgres + workers | Managed for you |
287
+ | **Base URL** | `http://localhost:3000/api/v1` | `https://cloud.meerkatagents.com/api/v1` (default) |
288
+
289
+ LLM usage is always billed by **your provider**. Meerkat never sees your model costs.
290
+
291
+ Self-host: [meerkat README — Docker](https://github.com/Tiny-Bubble-Company/meerkat#getting-started--docker-recommended)
292
+
293
+ ---
294
+
295
+ ## Installation
296
+
297
+ Add to your Gemfile:
298
+
299
+ ```ruby
300
+ gem "meerkat-agents"
301
+ ```
302
+
303
+ Or install directly:
304
+
305
+ ```bash
306
+ gem install meerkat-agents
307
+ ```
308
+
309
+ Then in your app:
310
+
311
+ ```ruby
312
+ require "meerkat"
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Quick start
318
+
319
+ ### 1. Get an API key
320
+
321
+ Sign up via the [Meerkat Cloud dashboard](https://cloud.meerkatagents.com/signup) or programmatically:
322
+
323
+ ```ruby
324
+ require "meerkat"
325
+
326
+ client = Meerkat::Client.new(base_url: "https://cloud.meerkatagents.com/api/v1")
327
+ result = client.signup.create(email: "you@company.com", name: "Your Name")
328
+ api_key = result.dig("data", "api_key") # store securely — shown once
329
+ ```
330
+
331
+ ### 2. Create a task
332
+
333
+ ```ruby
334
+ client = Meerkat::Client.new(api_key: ENV["MEERKAT_API_KEY"])
335
+
336
+ response = client.tasks.create(
337
+ task_type: "recurring",
338
+ description: "Monitor courier tracking and notify on status changes",
339
+ input_params: { courier_tracking_link: "https://..." },
340
+ frequency: "every 2 hours",
341
+ output_webhook: "https://your-app.com/webhooks/meerkat"
342
+ )
343
+
344
+ task = response["data"]
345
+ ```
346
+
347
+ ### 3. Trigger a run (optional)
348
+
349
+ ```ruby
350
+ client.tasks.run(task["id"], async: true)
351
+ ```
352
+
353
+ ---
354
+
355
+ ## Examples
356
+
357
+ ### List and filter tasks
358
+
359
+ ```ruby
360
+ client.tasks.list(status: "active", task_type: "recurring", limit: 20)
361
+ client.tasks.retrieve(1)
362
+ client.tasks.pause(1)
363
+ client.tasks.resume(1)
364
+ client.tasks.delete(1)
365
+ ```
366
+
367
+ ### One-off lookup
368
+
369
+ ```ruby
370
+ client.tasks.create(
371
+ task_type: "one_off",
372
+ description: "Fetch the current price of this product",
373
+ input_params: { url: "https://example.com/product/123" },
374
+ output_webhook: "https://your-app.com/webhooks/meerkat"
375
+ )
376
+ ```
377
+
378
+ ### Inspect runs and webhook delivery history
379
+
380
+ ```ruby
381
+ client.tasks.runs(1, limit: 10)
382
+ client.tasks.events(1, limit: 50)
383
+ ```
384
+
385
+ ### API keys
386
+
387
+ ```ruby
388
+ client.api_keys.list
389
+ client.api_keys.create(name: "Production")
390
+ client.api_keys.revoke(key_id)
391
+ ```
392
+
393
+ ---
394
+
395
+ ## Receiving webhooks (Rails)
396
+
397
+ Every outbound POST is HMAC-SHA256 signed in the `X-Meerkat-Signature` header. Verify before processing:
398
+
399
+ ```ruby
400
+ Meerkat::Webhooks.verify(
401
+ payload: request.raw_post,
402
+ signature: request.headers["X-Meerkat-Signature"],
403
+ secret: ENV["MEERKAT_WEBHOOK_SECRET"]
404
+ )
405
+ ```
406
+
407
+ ### Rails controller concern
408
+
409
+ ```ruby
410
+ class Webhooks::MeerkatController < ApplicationController
411
+ include Meerkat::Rails::WebhookVerification
412
+
413
+ def create
414
+ payload = JSON.parse(request.raw_post)
415
+ # handle event: run_completed, status_changed, etc.
416
+ head :ok
417
+ end
418
+ end
419
+ ```
420
+
421
+ Set `MEERKAT_WEBHOOK_SECRET` in your environment.
422
+
423
+ ---
424
+
425
+ ## Output formats
426
+
427
+ `output_webhook` is required on every task (or use `"default"` if your account has a default webhook configured). `output_format` is optional.
428
+
429
+ | Preset | Webhook payload shape |
430
+ |--------|----------------------|
431
+ | `default` | `{ event, task_id, task_run_id, occurred_at, data: { summary, findings, change_detected, ... } }` |
432
+ | `compact` | Top-level `summary`, `change_detected`, `findings` |
433
+ | `flat` | Findings merged into the webhook root object |
434
+ | `findings_only` | Findings object only |
435
+ | `minimal` | `event`, `task_id`, `summary`, `change_detected` |
436
+
437
+ ```ruby
438
+ client.tasks.create(
439
+ description: "...",
440
+ input_params: { url: "https://..." },
441
+ output_webhook: "https://your-app.com/hook",
442
+ output_format: "flat"
443
+ )
444
+ ```
445
+
446
+ Any other string is passed to the agent as a custom instruction for shaping findings (max 500 chars).
447
+
448
+ ---
449
+
450
+ ## API reference
451
+
452
+ Default base URL: `https://cloud.meerkatagents.com/api/v1`
453
+
454
+ All endpoints except signup require `Authorization: Bearer mk_...`.
455
+
456
+ | SDK method | HTTP | Description |
457
+ |------------|------|-------------|
458
+ | `client.signup.create(...)` | `POST /signup` | Create account + API key |
459
+ | `client.api_keys.list` | `GET /api_keys` | List API keys |
460
+ | `client.api_keys.create(name:)` | `POST /api_keys` | Generate a new key |
461
+ | `client.api_keys.revoke(id)` | `DELETE /api_keys/:id` | Revoke a key |
462
+ | `client.tasks.list(...)` | `GET /tasks` | List tasks |
463
+ | `client.tasks.create(...)` | `POST /tasks` | Create a task |
464
+ | `client.tasks.retrieve(id)` | `GET /tasks/:id` | Task details + last known state |
465
+ | `client.tasks.update(id, ...)` | `PATCH /tasks/:id` | Partial update |
466
+ | `client.tasks.replace(id, ...)` | `PUT /tasks/:id` | Full replace |
467
+ | `client.tasks.delete(id)` | `DELETE /tasks/:id` | Archive or delete |
468
+ | `client.tasks.run(id, async:)` | `POST /tasks/:id/run` | Trigger on-demand run |
469
+ | `client.tasks.pause(id)` | `POST /tasks/:id/pause` | Pause recurring task |
470
+ | `client.tasks.resume(id)` | `POST /tasks/:id/resume` | Resume recurring task |
471
+ | `client.tasks.runs(id)` | `GET /tasks/:id/runs` | Run history |
472
+ | `client.tasks.events(id)` | `GET /tasks/:id/events` | Webhook delivery log |
473
+
474
+ Full OpenAPI spec: [`meerkat/openapi/openapi.yaml`](https://github.com/Tiny-Bubble-Company/meerkat/blob/main/openapi/openapi.yaml)
475
+
476
+ ---
477
+
478
+ ## Configuration
479
+
480
+ ### Environment variables
481
+
482
+ | Variable | Description |
483
+ |----------|-------------|
484
+ | `MEERKAT_API_KEY` | API key from signup (`mk_...`) |
485
+ | `MEERKAT_BASE_URL` | API base URL (default: Meerkat Cloud) |
486
+ | `MEERKAT_WEBHOOK_SECRET` | Secret for verifying inbound webhooks |
487
+
488
+ ### Rails / global config
489
+
490
+ ```ruby
491
+ Meerkat.configure do |config|
492
+ config.api_key = ENV["MEERKAT_API_KEY"]
493
+ config.base_url = ENV.fetch("MEERKAT_BASE_URL", Meerkat::Client::DEFAULT_BASE_URL)
494
+ end
495
+
496
+ Meerkat.configuration.client.tasks.list(status: "active")
497
+ ```
498
+
499
+ ### Self-hosted instance
500
+
501
+ ```ruby
502
+ client = Meerkat::Client.new(
503
+ api_key: ENV["MEERKAT_API_KEY"],
504
+ base_url: "http://localhost:3000/api/v1"
505
+ )
506
+ ```
507
+
508
+ ---
509
+
510
+ ## Development
511
+
512
+ ```bash
513
+ bundle install
514
+ bundle exec rake spec
515
+ ```
516
+
517
+ See [PUBLISHING.md](PUBLISHING.md) for local testing and RubyGems release steps.
518
+
519
+ ---
520
+
521
+ ## Related projects
522
+
523
+ - [meerkat](https://github.com/Tiny-Bubble-Company/meerkat) — open source API server
524
+ - [meerkat-python](https://github.com/Tiny-Bubble-Company/meerkat-python) — Python SDK
525
+ - [meerkat-javascript](https://github.com/Tiny-Bubble-Company/meerkat-javascript) — JavaScript/TypeScript SDK
526
+
527
+ ---
528
+
529
+ ## License
530
+
531
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "faraday"
5
+
6
+ module Meerkat
7
+ class Client
8
+ DEFAULT_BASE_URL = "https://cloud.meerkatagents.com/api/v1"
9
+ USER_AGENT = "meerkat-agents/#{Meerkat::VERSION}"
10
+
11
+ def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, timeout: 30, faraday: nil)
12
+ @api_key = api_key
13
+ @base_url = normalize_base_url(base_url)
14
+ @timeout = timeout
15
+ @connection = faraday || build_connection
16
+ end
17
+
18
+ def signup
19
+ @signup ||= Resources::Signup.new(self)
20
+ end
21
+
22
+ def tasks
23
+ @tasks ||= Resources::Tasks.new(self)
24
+ end
25
+
26
+ def api_keys
27
+ @api_keys ||= Resources::ApiKeys.new(self)
28
+ end
29
+
30
+ def get(path, params: {})
31
+ request(:get, path, params: params)
32
+ end
33
+
34
+ def post(path, body: nil, params: {})
35
+ request(:post, path, body: body, params: params)
36
+ end
37
+
38
+ def patch(path, body: nil)
39
+ request(:patch, path, body: body)
40
+ end
41
+
42
+ def put(path, body: nil)
43
+ request(:put, path, body: body)
44
+ end
45
+
46
+ def delete(path, params: {})
47
+ request(:delete, path, params: params)
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :api_key, :connection
53
+
54
+ def build_connection
55
+ Faraday.new(url: @base_url) do |f|
56
+ f.request :json
57
+ f.response :json, content_type: /\bjson$/, parser_options: { symbolize_names: false }
58
+ f.options.timeout = @timeout
59
+ f.options.open_timeout = 10
60
+ f.headers["User-Agent"] = USER_AGENT
61
+ f.headers["Accept"] = "application/json"
62
+ f.adapter Faraday.default_adapter
63
+ end
64
+ end
65
+
66
+ def normalize_base_url(base_url)
67
+ "#{base_url.to_s.chomp("/")}/"
68
+ end
69
+
70
+ def request(method, path, body: nil, params: {})
71
+ relative_path = path.start_with?("/") ? path[1..] : path
72
+
73
+ response = connection.public_send(method, relative_path) do |req|
74
+ req.headers["Authorization"] = "Bearer #{api_key}" if api_key
75
+ req.params.update(params) if params.any?
76
+ req.body = body if body
77
+ end
78
+
79
+ handle_response(response)
80
+ end
81
+
82
+ def handle_response(response)
83
+ return nil if response.status == 204
84
+
85
+ body = response.body
86
+ body = JSON.parse(body) if body.is_a?(String) && !body.empty?
87
+
88
+ case response.status
89
+ when 200, 201, 202
90
+ body
91
+ when 401
92
+ raise AuthenticationError.new(error_detail(body), status: 401, errors: body&.dig("errors"))
93
+ when 404
94
+ raise NotFoundError.new(error_detail(body), status: 404, errors: body&.dig("errors"))
95
+ when 422
96
+ raise ValidationError.new(error_detail(body), status: 422, errors: body&.dig("errors"))
97
+ else
98
+ raise ApiError.new(error_detail(body), status: response.status, errors: body&.dig("errors"))
99
+ end
100
+ end
101
+
102
+ def error_detail(body)
103
+ return "Request failed" unless body.is_a?(Hash)
104
+
105
+ body.dig("errors", 0, "detail") || body.dig("errors", 0, "title") || "Request failed"
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meerkat
4
+ class << self
5
+ def configure
6
+ yield configuration if block_given?
7
+ configuration
8
+ end
9
+
10
+ def configuration
11
+ @configuration ||= Configuration.new
12
+ end
13
+ end
14
+
15
+ class Configuration
16
+ attr_accessor :api_key, :base_url
17
+
18
+ def initialize
19
+ @api_key = ENV["MEERKAT_API_KEY"]
20
+ @base_url = ENV.fetch("MEERKAT_BASE_URL", Client::DEFAULT_BASE_URL)
21
+ end
22
+
23
+ def client
24
+ Client.new(api_key: api_key, base_url: base_url)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meerkat
4
+ class Error < StandardError
5
+ attr_reader :status, :errors
6
+
7
+ def initialize(message = nil, status: nil, errors: nil)
8
+ super(message)
9
+ @status = status
10
+ @errors = errors
11
+ end
12
+ end
13
+
14
+ class AuthenticationError < Error; end
15
+ class NotFoundError < Error; end
16
+ class ValidationError < Error; end
17
+ class ApiError < Error; end
18
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "meerkat/webhooks"
4
+
5
+ module Meerkat
6
+ module Rails
7
+ module WebhookVerification
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ before_action :verify_meerkat_webhook!, only: [ :create ]
12
+ end
13
+
14
+ private
15
+
16
+ def verify_meerkat_webhook!
17
+ secret = meerkat_webhook_secret
18
+ return if secret.blank?
19
+
20
+ signature = request.headers[Webhooks::SIGNATURE_HEADER]
21
+ unless Webhooks.verify(payload: request.raw_post, signature: signature, secret: secret)
22
+ head :unauthorized
23
+ end
24
+ end
25
+
26
+ def meerkat_webhook_secret
27
+ ENV["MEERKAT_WEBHOOK_SECRET"]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meerkat
4
+ class Railtie < ::Rails::Railtie
5
+ initializer "meerkat.configure" do
6
+ Meerkat.configure
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meerkat
4
+ module Resources
5
+ class ApiKeys < Base
6
+ def list
7
+ client.get("/api_keys")
8
+ end
9
+
10
+ def create(name: "Default")
11
+ client.post("/api_keys", body: { api_key: { name: name } })
12
+ end
13
+
14
+ def revoke(id)
15
+ client.delete("/api_keys/#{id}")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meerkat
4
+ module Resources
5
+ class Base
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ private
11
+
12
+ attr_reader :client
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meerkat
4
+ module Resources
5
+ class Signup < Base
6
+ def create(email:, name: nil, company: nil)
7
+ client.post("/signup", body: {
8
+ customer: {
9
+ email: email,
10
+ name: name,
11
+ company: company
12
+ }.compact
13
+ })
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meerkat
4
+ module Resources
5
+ class Tasks < Base
6
+ def list(task_type: nil, status: nil, include_archived: nil, limit: nil, offset: nil)
7
+ client.get("/tasks", params: {
8
+ task_type: task_type,
9
+ status: status,
10
+ include_archived: include_archived,
11
+ limit: limit,
12
+ offset: offset
13
+ }.compact)
14
+ end
15
+
16
+ def retrieve(id)
17
+ client.get("/tasks/#{id}")
18
+ end
19
+
20
+ def create(**attributes)
21
+ client.post("/tasks", body: { task: attributes })
22
+ end
23
+
24
+ def update(id, **attributes)
25
+ client.patch("/tasks/#{id}", body: { task: attributes })
26
+ end
27
+
28
+ def replace(id, **attributes)
29
+ client.put("/tasks/#{id}", body: { task: attributes })
30
+ end
31
+
32
+ def delete(id, permanent: false)
33
+ client.delete("/tasks/#{id}", params: { permanent: permanent })
34
+ end
35
+
36
+ def run(id, async: true)
37
+ client.post("/tasks/#{id}/run", params: { async: async })
38
+ end
39
+
40
+ def pause(id)
41
+ client.post("/tasks/#{id}/pause")
42
+ end
43
+
44
+ def resume(id)
45
+ client.post("/tasks/#{id}/resume")
46
+ end
47
+
48
+ def runs(id, limit: nil)
49
+ client.get("/tasks/#{id}/runs", params: { limit: limit }.compact)
50
+ end
51
+
52
+ def events(id, limit: nil)
53
+ client.get("/tasks/#{id}/events", params: { limit: limit }.compact)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meerkat
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Meerkat
6
+ module Webhooks
7
+ SIGNATURE_HEADER = "X-Meerkat-Signature"
8
+ EVENT_HEADER = "X-Meerkat-Event"
9
+
10
+ module_function
11
+
12
+ def sign(payload:, secret:)
13
+ digest = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
14
+ "sha256=#{digest}"
15
+ end
16
+
17
+ def verify(payload:, signature:, secret:)
18
+ return false if signature.nil? || signature.empty?
19
+
20
+ expected = sign(payload: payload, secret: secret)
21
+ secure_compare(signature, expected)
22
+ end
23
+
24
+ def secure_compare(provided, expected)
25
+ return false unless provided.bytesize == expected.bytesize
26
+
27
+ result = 0
28
+ provided.bytes.zip(expected.bytes) { |a, b| result |= a ^ b }
29
+ result.zero?
30
+ end
31
+ private_class_method :secure_compare
32
+ end
33
+ end
data/lib/meerkat.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "meerkat/version"
4
+ require_relative "meerkat/error"
5
+ require_relative "meerkat/webhooks"
6
+ require_relative "meerkat/client"
7
+ require_relative "meerkat/configuration"
8
+ require_relative "meerkat/resources/base"
9
+ require_relative "meerkat/resources/signup"
10
+ require_relative "meerkat/resources/api_keys"
11
+ require_relative "meerkat/resources/tasks"
12
+
13
+ if defined?(Rails::Railtie)
14
+ require_relative "meerkat/railtie"
15
+ require_relative "meerkat/rails/webhook_verification"
16
+ end
17
+
18
+ module Meerkat
19
+ class << self
20
+ def client(**options)
21
+ Client.new(**options)
22
+ end
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: meerkat-agents
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tiny Bubble Company
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.9'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.9'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ description: |
34
+ Meerkat is an open-source, webhook-native API for async agent tasks. Monitor any website for changes,
35
+ track shipments, watch competitor prices and stock, or schedule recurring scraping jobs — and get an
36
+ HMAC-signed webhook when something happens. BYOK, no schedulers or webhook plumbing to build yourself.
37
+ email:
38
+ - hello@meerkatagents.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - ".github/meerkat-logo.png"
44
+ - ".github/readme-banner.png"
45
+ - CHANGELOG.md
46
+ - LICENSE
47
+ - README.md
48
+ - lib/meerkat.rb
49
+ - lib/meerkat/client.rb
50
+ - lib/meerkat/configuration.rb
51
+ - lib/meerkat/error.rb
52
+ - lib/meerkat/rails/webhook_verification.rb
53
+ - lib/meerkat/railtie.rb
54
+ - lib/meerkat/resources/api_keys.rb
55
+ - lib/meerkat/resources/base.rb
56
+ - lib/meerkat/resources/signup.rb
57
+ - lib/meerkat/resources/tasks.rb
58
+ - lib/meerkat/version.rb
59
+ - lib/meerkat/webhooks.rb
60
+ homepage: https://github.com/Tiny-Bubble-Company/meerkat-ruby
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ source_code_uri: https://github.com/Tiny-Bubble-Company/meerkat-ruby
65
+ changelog_uri: https://github.com/Tiny-Bubble-Company/meerkat-ruby/blob/main/CHANGELOG.md
66
+ rubygems_mfa_required: 'true'
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.2'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.4.1
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Async agent task API — monitor websites for changes, track deliveries, watch
86
+ prices/stock via webhooks.
87
+ test_files: []