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 +7 -0
- data/.github/meerkat-logo.png +0 -0
- data/.github/readme-banner.png +0 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE +21 -0
- data/README.md +531 -0
- data/lib/meerkat/client.rb +108 -0
- data/lib/meerkat/configuration.rb +27 -0
- data/lib/meerkat/error.rb +18 -0
- data/lib/meerkat/rails/webhook_verification.rb +31 -0
- data/lib/meerkat/railtie.rb +9 -0
- data/lib/meerkat/resources/api_keys.rb +19 -0
- data/lib/meerkat/resources/base.rb +15 -0
- data/lib/meerkat/resources/signup.rb +17 -0
- data/lib/meerkat/resources/tasks.rb +57 -0
- data/lib/meerkat/version.rb +5 -0
- data/lib/meerkat/webhooks.rb +33 -0
- data/lib/meerkat.rb +24 -0
- metadata +87 -0
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
|
+
[](https://badge.fury.io/rb/meerkat-agents)
|
|
26
|
+
[](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,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,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,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: []
|