misarseo 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/LICENSE +21 -0
  4. data/README.md +257 -0
  5. data/lib/misarseo/client.rb +29 -0
  6. data/lib/misarseo/errors.rb +100 -0
  7. data/lib/misarseo/generated/account.rb +30 -0
  8. data/lib/misarseo/generated/activity.rb +22 -0
  9. data/lib/misarseo/generated/ahrefs.rb +35 -0
  10. data/lib/misarseo/generated/ai_search.rb +87 -0
  11. data/lib/misarseo/generated/backlinks.rb +34 -0
  12. data/lib/misarseo/generated/baidu.rb +35 -0
  13. data/lib/misarseo/generated/bing.rb +95 -0
  14. data/lib/misarseo/generated/brave_search.rb +20 -0
  15. data/lib/misarseo/generated/competitor_monitor.rb +37 -0
  16. data/lib/misarseo/generated/competitors.rb +20 -0
  17. data/lib/misarseo/generated/content.rb +25 -0
  18. data/lib/misarseo/generated/crawl.rb +140 -0
  19. data/lib/misarseo/generated/dashboard.rb +20 -0
  20. data/lib/misarseo/generated/direct_vendor_ai.rb +20 -0
  21. data/lib/misarseo/generated/disavow.rb +35 -0
  22. data/lib/misarseo/generated/domain.rb +37 -0
  23. data/lib/misarseo/generated/ga4.rb +50 -0
  24. data/lib/misarseo/generated/google_business_profile.rb +77 -0
  25. data/lib/misarseo/generated/gsc.rb +70 -0
  26. data/lib/misarseo/generated/index_now.rb +20 -0
  27. data/lib/misarseo/generated/keywords.rb +71 -0
  28. data/lib/misarseo/generated/link_building.rb +60 -0
  29. data/lib/misarseo/generated/local.rb +30 -0
  30. data/lib/misarseo/generated/local_pack_rank.rb +40 -0
  31. data/lib/misarseo/generated/local_rank_grid.rb +40 -0
  32. data/lib/misarseo/generated/log_analyzer.rb +42 -0
  33. data/lib/misarseo/generated/mentions.rb +25 -0
  34. data/lib/misarseo/generated/patches.rb +55 -0
  35. data/lib/misarseo/generated/portfolios.rb +50 -0
  36. data/lib/misarseo/generated/projects.rb +40 -0
  37. data/lib/misarseo/generated/rank_tracking.rb +144 -0
  38. data/lib/misarseo/generated/report_builder.rb +67 -0
  39. data/lib/misarseo/generated/resources.rb +85 -0
  40. data/lib/misarseo/generated/team.rb +20 -0
  41. data/lib/misarseo/generated/usage.rb +20 -0
  42. data/lib/misarseo/generated/web_analytics.rb +35 -0
  43. data/lib/misarseo/generated/yandex.rb +45 -0
  44. data/lib/misarseo/pagination.rb +38 -0
  45. data/lib/misarseo/transport.rb +192 -0
  46. data/lib/misarseo/version.rb +5 -0
  47. data/lib/misarseo.rb +13 -0
  48. data/manifest.json +2850 -0
  49. metadata +114 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 31f9863c4b7f4723163ddbca2c716dd97cc8b9ede908e30569397d492feb5453
4
+ data.tar.gz: f33667cb53a6b0b683d1c412714c1c124cf33c9c507be0635c3b3e9243002f9a
5
+ SHA512:
6
+ metadata.gz: 6ccc2a49b94d5bb5fc479e7ff5d9f731af485c805dad52c54e53d2a92d8e0063521646be5e241643efef18946b1913976afc8e8d8289ae1c70d707bd6d40b23b
7
+ data.tar.gz: 5a77b689e14b3a3b214544cfd6cfa7539f6a028b444700358a4efbce44fd11dbcd6ea3485fe37a2d653d31058473e8add6859096226f8c47b3abe3657ffe1df7
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this SDK are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.0.0] — 2026-10-16
8
+
9
+ First release.
10
+
11
+ ### Added
12
+
13
+ - The full MisarSEO API: 221 methods in 36 resource groups, generated from
14
+ `manifest.json` (itself generated from the API's OpenAPI route registry),
15
+ plus the crawl progress Server-Sent Events stream.
16
+ - Bearer API-key authentication (`mseo_…`), with the key read from
17
+ `MISARSEO_API_KEY` where the platform has an environment.
18
+ - A plan-limit error carrying the server's upgrade offer (`code:
19
+ "plan_limit_exceeded"`; 402 for a locked feature or a point-in-time cap,
20
+ 429 with `Retry-After` for a spent quota), detected by code and never
21
+ retried.
22
+ - The shared retry policy: 429 for every method, 502/503/504 and network
23
+ failures for GET only, exponential backoff with jitter honouring
24
+ `Retry-After` up to 60 s.
25
+ - Offset and cursor pagination helpers.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Misar.AI
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,257 @@
1
+ # MisarSEO Ruby SDK
2
+
3
+ > Keyword research, site audits, rank tracking and AI search visibility from Ruby.
4
+
5
+ [![Gem](https://img.shields.io/gem/v/misarseo)](https://rubygems.org/gems/misarseo)
6
+ [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
7
+
8
+ **36 resource groups · 221 methods · SSE crawl streaming · standard library only**
9
+
10
+ MisarSEO is one API for the SEO work around a site: keyword and competitor research, crawls and audits, rank tracking (organic, local and AI Overviews), backlinks, AI-search brand visibility, and the data Google Search Console, GA4, Bing, Yandex and Baidu hold about it. Ruby 2.6+. Every method returns the parsed JSON (Hash/Array).
11
+
12
+ Package: `misarseo` on RubyGems. Source: [github.com/Misar-AI/misarseo-sdks](https://github.com/Misar-AI/misarseo-sdks/tree/main/ruby).
13
+
14
+ ---
15
+
16
+ ## Install
17
+
18
+ ### Bundler
19
+
20
+ ```ruby
21
+ gem "misarseo"
22
+ ```
23
+
24
+ ### gem
25
+
26
+ ```bash
27
+ gem install misarseo
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Authentication
33
+
34
+ Create an API key as described at https://seo.misar.io/ai. It starts with `mseo_` and is sent as `Authorization: Bearer mseo_…` — the only credential the SDK uses.
35
+
36
+ Every call is decided server-side against the plan of the organization that owns the key; there is no client-side limit checking. A plan refusal carries `code: "plan_limit_exceeded"` and an upgrade offer, and is never retried (see [Plan limits](#plan-limits)).
37
+
38
+ ```ruby
39
+ require "misarseo"
40
+
41
+ seo = MisarSEO::Client.new(api_key: ENV["MISARSEO_API_KEY"])
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Resources
47
+
48
+ Every group the client exposes, and every method on it — generated from [`manifest.json`](./manifest.json), the same file every MisarSEO SDK is built from. Positional arguments: path parameters then required query parameters, then the JSON body (when the endpoint takes one); `query:` takes optional query parameters.
49
+
50
+ ### Workspace
51
+
52
+ | Resource | Methods | What it covers |
53
+ | --- | --- | --- |
54
+ | `seo.projects` | `create`, `delete`, `get`, `list`, `update` | Projects in the organization: list, create, rename, archive. |
55
+ | `seo.team` | `list_members` | Members, pending invitations and the caller's role (read-only). |
56
+ | `seo.account` | `delete`, `delete_via_post`, `export` | Data-subject rights: full JSON export and account erasure. |
57
+ | `seo.activity` | `list` | The organization's activity log, newest first (cursor paged). |
58
+ | `seo.usage` | `get` | Plan tier, credits and quota usage for the organization. |
59
+ | `seo.dashboard` | `get` | Cross-project overview — one card per active project. |
60
+ | `seo.portfolios` | `add_projects`, `create`, `delete`, `get`, `list`, `remove_projects`, `rename` | Group projects and read their aggregate summary. |
61
+ | `seo.report_builder` | `create_definition`, `delete_definition`, `delete_schedule`, `get_definition`, `get_schedule`, `list_definitions`, `rename_definition`, `render_definition`, `set_widgets`, `upsert_schedule` | User-composed report definitions, widgets, rendering and schedules. |
62
+
63
+ ### Keyword and competitive research
64
+
65
+ | Resource | Methods | What it covers |
66
+ | --- | --- | --- |
67
+ | `seo.keywords` | `classify_intent`, `gap`, `list_saved`, `list_share_of_voice`, `potential`, `research`, `save`, `serp_overview`, `strategy`, `topic_research` | Keyword research, saved keywords, intent, SERP overview, gap, topic research, strategy, share of voice. |
68
+ | `seo.domain` | `authority_history`, `batch_overview`, `capture_authority_snapshot`, `overview` | Domain overview, batch overview and authority history. |
69
+ | `seo.backlinks` | `anchors`, `intersect`, `overview` | Backlink profile, anchors (offset paged) and link intersect. |
70
+ | `seo.competitors` | `find` | SERP competitor discovery. |
71
+ | `seo.content` | `optimizer`, `page_metrics` | On-page content optimizer and page metrics. |
72
+ | `seo.mentions` | `media_monitoring`, `unlinked_mentions` | Unlinked brand mentions and media monitoring. |
73
+ | `seo.competitor_monitor` | `add_target`, `list_changes`, `list_targets`, `remove_target` | Competitor page change detection. |
74
+ | `seo.link_building` | `add`, `add_note`, `export_csv`, `import_csv`, `list`, `list_activity`, `remove`, `update`, `update_status` | Prospect list, outreach status, activity log, CSV import/export. |
75
+ | `seo.disavow` | `add`, `export`, `list`, `remove` | Disavow entries and Google's disavow-file export. |
76
+
77
+ ### Site audit
78
+
79
+ | Resource | Methods | What it covers |
80
+ | --- | --- | --- |
81
+ | `seo.crawl` | `clear_issue_status`, `comparison`, `core_web_vitals`, `create_audit_segment`, `create_custom_issue_rule`, `create_issue_mute_rule`, `create_issue_severity_override`, `delete_audit_segment`, `delete_custom_issue_rule`, `delete_issue_mute_rule`, `delete_issue_severity_override`, `https_report`, `internal_links`, `list_audit_segments`, `list_custom_issue_rules`, `list_issue_mute_rules`, `list_issue_severity_overrides`, `list_issue_statuses`, `segment`, `set_issue_status`, `start`, `status`, `structured_data_report`, `stream_events` (SSE) | Start a crawl, read status and comparisons, Core Web Vitals, triage rules, audit reports, and the live SSE progress stream. |
82
+ | `seo.patches` | `batch_draft`, `disable_auto_publish`, `draft`, `enable_auto_publish`, `list`, `list_auto_publish_rules`, `publish`, `revert` | AI-drafted title/meta/canonical patches, publish/revert and auto-publish rules. |
83
+ | `seo.log_analyzer` | `daily_summary`, `get_upload`, `list_uploads`, `top_paths`, `upload` | Server-log uploads and bot-traffic reports. |
84
+ | `seo.web_analytics` | `devices`, `sources`, `summary`, `top_pages` | First-party, cookieless traffic and AI-referral reports. |
85
+
86
+ ### Rank tracking
87
+
88
+ | Resource | Methods | What it covers |
89
+ | --- | --- | --- |
90
+ | `seo.rank_tracking` | `add_annotation`, `add_competitor`, `add_keywords`, `create`, `delete`, `get`, `get_ai_overview_trend`, `get_config_trend`, `get_keyword_history`, `get_latest_run`, `get_rank_movers`, `get_volatility_index`, `list_annotations`, `list_cannibalisation`, `list_competitors`, `list_configs`, `list_keyword_tags`, `remove_annotation`, `remove_competitor`, `remove_keywords`, `set_keyword_target_url`, `update`, `update_keyword_tag` | Trackers, keywords, tags, runs, trends, movers, annotations, cannibalisation, competitors. |
91
+ | `seo.local_rank_grid` | `create_config`, `delete_config`, `get_latest_run`, `list_configs`, `run_check` | Geo-grid organic rank tracking. |
92
+ | `seo.local_pack_rank` | `create_config`, `delete_config`, `get_latest_check`, `list_configs`, `run_check` | Local-pack position of a named business. |
93
+ | `seo.brave_search` | `rank_check` | Rank checks against Brave Search's independent index. |
94
+
95
+ ### Local
96
+
97
+ | Resource | Methods | What it covers |
98
+ | --- | --- | --- |
99
+ | `seo.local` | `business_listings`, `nap_audit`, `questions_answers` | Business listings, Q&A and NAP audit. |
100
+ | `seo.google_business_profile` | `available_locations`, `create_post`, `delete_review_reply`, `get_connection`, `get_optimization`, `get_performance`, `get_profile`, `link`, `list_posts`, `list_reviews`, `reply_to_review`, `unlink` | Business Profile connection, profile, performance, reviews, posts, optimization. |
101
+
102
+ ### AI search visibility
103
+
104
+ | Resource | Methods | What it covers |
105
+ | --- | --- | --- |
106
+ | `seo.ai_search` | `brand_lookup`, `brand_lookup_status`, `brand_scan_trend`, `citations`, `competitor_positioning`, `delete_brand_monitor`, `delete_tracked_prompt`, `list_brand_monitors`, `list_tracked_prompts`, `prompt_explorer`, `prompt_research`, `tracked_prompt_trend`, `upsert_brand_monitor`, `upsert_tracked_prompt` | Brand lookup, prompt explorer, brand monitors, tracked prompts, competitor positioning, prompt research, citations. |
107
+ | `seo.direct_vendor_ai` | `answer` | Answers from vendors' own APIs (You.com, DeepSeek, xAI, Meta AI, MiniMax). |
108
+
109
+ ### Search engines and analytics
110
+
111
+ | Resource | Methods | What it covers |
112
+ | --- | --- | --- |
113
+ | `seo.gsc` | `anonymous_queries`, `archive`, `cannibalisation`, `connect`, `content_decay`, `coverage`, `ctr_by_position`, `hourly`, `inspect_urls`, `performance`, `sitemaps` | Search Console performance, archive, coverage, URL inspection, sitemaps, reports. |
114
+ | `seo.ga4` | `channels`, `connect`, `daily`, `disconnect`, `landing_pages`, `list_properties`, `set_property` | Google Analytics 4 connection and reports. |
115
+ | `seo.bing` | `backlinks`, `connect`, `detail_stats`, `disconnect`, `get_feed_details`, `keyword_research`, `list_sites`, `save_crawl_settings`, `set_site`, `site_overview`, `submit_content`, `submit_feed`, `update_blocked_url`, `update_query_parameter`, `url_info`, `url_links` | Bing Webmaster: site overview, backlinks, keyword research, submissions, settings. |
116
+ | `seo.yandex` | `connect`, `disconnect`, `list_hosts`, `reindex_url`, `set_host`, `site_overview` | Yandex Webmaster: site overview, re-index, connection. |
117
+ | `seo.baidu` | `connection`, `disconnect`, `set_connection`, `submit_urls` | Baidu push API connection and URL submission. |
118
+ | `seo.index_now` | `submit` | IndexNow URL submission. |
119
+ | `seo.ahrefs` | `connect`, `disconnect`, `list_projects`, `set_project` | Ahrefs Connect project selection (inert until partner approval). |
120
+
121
+
122
+ ---
123
+
124
+ ## Client
125
+
126
+ ```ruby
127
+ MisarSEO::Client.new(
128
+ api_key: "mseo_…",
129
+ base_url: "https://api.misar.io/seo", # default
130
+ timeout: 30,
131
+ max_retries: 2
132
+ )
133
+ ```
134
+
135
+ | Option | Default | Meaning |
136
+ | --- | --- | --- |
137
+ | base URL | `https://api.misar.io/seo` | API gateway. |
138
+ | timeout | 30 s | Per request; a stream's wait covers only the response headers. |
139
+ | max retries | 2 | Extra attempts for retryable failures (see [Retries](#retries)). |
140
+ | retry delays | 250 ms base, 60 s cap | Exponential backoff with jitter; `Retry-After` honoured up to the cap. |
141
+
142
+ ---
143
+
144
+ ## Quick start
145
+
146
+ ```ruby
147
+ project_id = seo.projects.list["projects"].first["id"]
148
+
149
+ ideas = seo.keywords.research("projectId" => project_id, "keywords" => ["seo tools"])
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Primary functions
155
+
156
+ ### Run a site audit and follow it live
157
+
158
+ ```ruby
159
+ job = seo.crawl.start("projectId" => project_id, "startUrl" => "https://example.com")
160
+
161
+ seo.crawl.stream_events(job["jobId"], project_id) do |frame|
162
+ puts "#{frame.event} #{frame.data}" # progress … then complete or error
163
+ end
164
+ ```
165
+
166
+ ### Read a backlink profile
167
+
168
+ ```ruby
169
+ profile = seo.backlinks.overview(project_id, "example.com")
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Errors
175
+
176
+ ```ruby
177
+ begin
178
+ seo.projects.get("nope")
179
+ rescue MisarSEO::NotFoundError => e
180
+ puts e.code # "NOT_FOUND"
181
+ rescue MisarSEO::Error => e
182
+ puts "#{e.status} #{e.code}"
183
+ end
184
+ ```
185
+
186
+ | Type | When |
187
+ | --- | --- |
188
+ | `MisarSEO::Error` | Base: `status`, `code`, `body`, `retry_after`, `not_found?`, `rate_limited?`. |
189
+ | `MisarSEO::PlanLimitError` | `offer`, `reason`, `feature`, `plan`, `upgrade_url`. |
190
+ | `ValidationError` … `NotFoundError`, `RateLimitError`, `UpstreamError`, `NetworkError` | 400–404, 429, 502–504, no response. |
191
+
192
+ ### Plan limits
193
+
194
+ A call the organization's plan does not cover answers with
195
+
196
+ ```json
197
+ { "error": "...", "code": "plan_limit_exceeded", "upgrade": { "reason", "feature", "headline", "plans": [...], "urls": { "pricing" }, "cta" } }
198
+ ```
199
+
200
+ - **402** — the feature is not in the plan, or a point-in-time cap (projects, API keys, tracked keywords) is reached.
201
+ - **429** with `Retry-After` — a day/month quota is spent; it resets at the period rollover.
202
+
203
+ Detection is by `code`, never by status: a quota block is a 429 and would otherwise look like an ordinary throttle. Every piece of copy — plan names, prices, what each plan unlocks, URLs — is in the offer, so render it rather than hard-coding any of it. `x-misar-plan` and `x-misar-upgrade-url` response headers carry the current plan and upgrade link.
204
+
205
+ ```ruby
206
+ begin
207
+ seo.keywords.research("projectId" => project_id, "keywords" => ["seo"])
208
+ rescue MisarSEO::PlanLimitError => e
209
+ puts e.offer["headline"], e.upgrade_url
210
+ end
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Retries
216
+
217
+ - **429** is retried for every method (the limiter refuses before any work is done) — except a `plan_limit_exceeded` body, which is never retried.
218
+ - **502/503/504 and network failures** are retried for GET only: a POST may already have started a billed crawl.
219
+ - Exponential backoff from 250 ms with jitter; a `Retry-After` longer than the cap raises immediately instead of parking the caller.
220
+ - 400/401/402/403/404/409 and 500/501 are never retried — they fail the same way every time.
221
+
222
+ ---
223
+
224
+ ## Pagination
225
+
226
+ The API pages two ways: offset pages (`{ rows, hasMore, page }`, e.g. backlink anchors) and keyset pages (`{ rows, hasMore, nextCursor }`, e.g. the activity log, whose rows are under `entries`). The helpers stop on `hasMore: false`, on an empty page, or on a repeated cursor, so a shrinking result set cannot loop forever against a billed endpoint.
227
+
228
+ ```ruby
229
+ anchors = MisarSEO::Pagination.collect_offset_pages do |page|
230
+ seo.backlinks.anchors(project_id, "example.com", query: { "page" => page })
231
+ end
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Streaming
237
+
238
+ `crawl` progress is a Server-Sent Events stream (`GET /crawl/:jobId/stream`, `Accept: text/event-stream`): `progress` frames roughly every 1.5 s, then one terminal `complete` or `error`, after which the SDK ends the stream. It is never retried — replaying a stream would duplicate what was already consumed. See [Run a site audit](#run-a-site-audit-and-follow-it-live).
239
+
240
+ ---
241
+
242
+ ## Versioning
243
+
244
+ Semantic versioning; see [CHANGELOG.md](./CHANGELOG.md). Every SDK is generated from the same `manifest.json` (API 1.1.0), so a method added to the API appears in all thirteen at once.
245
+
246
+ ---
247
+
248
+ ## Links
249
+
250
+ - Product: https://seo.misar.io
251
+ - API docs: https://docs.misar.io/seo
252
+ - Issues: https://github.com/Misar-AI/misarseo-sdks/issues
253
+ - Status: https://status.misar.io
254
+
255
+ ## License
256
+
257
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MisarSEO
4
+ # MisarSEO API client. Every resource is a method named after it —
5
+ # +client.projects+, +client.rank_tracking+, +client.ai_search+ …
6
+ #
7
+ # client = MisarSEO::Client.new(api_key: ENV["MISARSEO_API_KEY"])
8
+ # client.projects.list
9
+ class Client
10
+ Generated::RESOURCES.each do |name, klass|
11
+ define_method(name) { @resources[name] ||= klass.new(@transport) }
12
+ end
13
+
14
+ # +api_key+ falls back to MISARSEO_API_KEY. +adapter+ replaces Net::HTTP (tests).
15
+ def initialize(api_key: nil, base_url: Transport::DEFAULT_BASE_URL, timeout: 30, max_retries: 2,
16
+ retry_base_delay: 0.25, max_retry_delay: 60, adapter: nil)
17
+ key = api_key.to_s.empty? ? ENV["MISARSEO_API_KEY"].to_s : api_key
18
+ if key.empty?
19
+ raise ArgumentError, "A MisarSEO API key (mseo_...) is required: pass api_key: or set " \
20
+ "MISARSEO_API_KEY; see https://seo.misar.io/ai"
21
+ end
22
+
23
+ @transport = Transport.new(api_key: key, base_url: base_url, timeout: timeout, max_retries: max_retries,
24
+ retry_base_delay: retry_base_delay, max_retry_delay: max_retry_delay,
25
+ adapter: adapter)
26
+ @resources = {}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MisarSEO
4
+ # The +code+ every Misar product sends on a plan-limit rejection.
5
+ PLAN_LIMIT_CODE = "plan_limit_exceeded"
6
+
7
+ # A non-2xx response, or (status 0) a request that never got one. +code+ is
8
+ # the API's own code — NOT_FOUND, VALIDATION_ERROR, RATE_LIMITED …
9
+ class Error < StandardError
10
+ attr_reader :status, :code, :body, :retry_after
11
+
12
+ def initialize(status, code, message, body = nil, retry_after = nil)
13
+ @status = status
14
+ @code = code
15
+ @body = body
16
+ @retry_after = retry_after
17
+ super(status.zero? ? "misarseo: #{message}" : "misarseo: HTTP #{status} (#{code}): #{message}")
18
+ end
19
+
20
+ def not_found?
21
+ status == 404
22
+ end
23
+
24
+ def unauthorized?
25
+ status == 401
26
+ end
27
+
28
+ # An ordinary throttle — never true for a plan limit.
29
+ def rate_limited?
30
+ status == 429 && !is_a?(PlanLimitError)
31
+ end
32
+ end
33
+
34
+ class ValidationError < Error; end
35
+ class AuthenticationError < Error; end
36
+ class PaymentRequiredError < Error; end
37
+ class PermissionError < Error; end
38
+ class NotFoundError < Error; end
39
+ class RateLimitError < Error; end
40
+ class UpstreamError < Error; end
41
+ class NetworkError < Error; end
42
+
43
+ # The organization's plan does not cover the call: a feature its tier locks
44
+ # (402), a spent day/month quota (429 with Retry-After at the rollover) or a
45
+ # point-in-time cap such as projects (402). Never retried — show +offer+, or
46
+ # at least +upgrade_url+, to the user.
47
+ class PlanLimitError < Error
48
+ # The server's +upgrade+ Hash: plans, prices, URLs — all server-side copy.
49
+ attr_reader :offer
50
+ # Current plan slug (x-misar-plan, else the offer).
51
+ attr_reader :plan
52
+ # Where to upgrade (x-misar-upgrade-url, else the offer).
53
+ attr_reader :upgrade_url
54
+
55
+ def initialize(status, message, body, retry_after, plan_header, upgrade_url_header)
56
+ upgrade = body.is_a?(Hash) ? body["upgrade"] : nil
57
+ @offer = upgrade.is_a?(Hash) ? upgrade : nil
58
+ @plan = plan_header || dig(@offer, "current_plan", "slug")
59
+ @upgrade_url = upgrade_url_header || dig(@offer, "urls", "pricing")
60
+ super(status, PLAN_LIMIT_CODE, message, body, retry_after)
61
+ end
62
+
63
+ # quota_exhausted, feature_locked …
64
+ def reason
65
+ dig(@offer, "reason")
66
+ end
67
+
68
+ # Machine key of the limit that was hit.
69
+ def feature
70
+ dig(@offer, "feature")
71
+ end
72
+
73
+ private
74
+
75
+ def dig(hash, *path)
76
+ path.reduce(hash) { |cur, key| cur.is_a?(Hash) ? cur[key] : nil }
77
+ end
78
+ end
79
+
80
+ STATUS_ERRORS = {
81
+ 400 => ValidationError, 401 => AuthenticationError, 402 => PaymentRequiredError,
82
+ 403 => PermissionError, 404 => NotFoundError, 429 => RateLimitError,
83
+ 502 => UpstreamError, 503 => UpstreamError, 504 => UpstreamError
84
+ }.freeze
85
+
86
+ # The error for a non-2xx response. The plan-limit code is checked BEFORE the
87
+ # status: a spent quota is a 429 that otherwise looks like an ordinary throttle.
88
+ def self.error_from(status, headers, body)
89
+ data = body.is_a?(Hash) ? body : {}
90
+ message = (data["error"] || data["message"] || "HTTP #{status}").to_s
91
+ code = (data["code"] || "API_ERROR").to_s
92
+ h = headers.each_with_object({}) { |(k, v), acc| acc[k.to_s.downcase] = v.is_a?(Array) ? v.first : v }
93
+ retry_after = h["retry-after"].to_s.strip =~ /\A\d+\z/ ? h["retry-after"].to_i : nil
94
+ if code == PLAN_LIMIT_CODE
95
+ return PlanLimitError.new(status, message, body, retry_after, h["x-misar-plan"], h["x-misar-upgrade-url"])
96
+ end
97
+
98
+ STATUS_ERRORS.fetch(status, Error).new(status, code, message, body, retry_after)
99
+ end
100
+ end
@@ -0,0 +1,30 @@
1
+ # AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
2
+ # Do not edit by hand: changes are overwritten. To change this surface,
3
+ # change the API route and its registry entry, run `pnpm sdk:sync`, then
4
+ # `pnpm sdk:generate`.
5
+
6
+ module MisarSEO
7
+ module Generated
8
+ # +client.account+ — 3 endpoint(s).
9
+ class AccountResource
10
+ def initialize(transport)
11
+ @transport = transport
12
+ end
13
+
14
+ # DELETE /account/delete
15
+ def delete(body = nil, query: nil, **fields)
16
+ @transport.request("DELETE", "/account/delete", query: query, body: Transport.body(body, fields))
17
+ end
18
+
19
+ # POST /account/delete
20
+ def delete_via_post(body = nil, query: nil, **fields)
21
+ @transport.request("POST", "/account/delete", query: query, body: Transport.body(body, fields))
22
+ end
23
+
24
+ # GET /account/export
25
+ def export(query: nil)
26
+ @transport.request("GET", "/account/export", query: query, body: nil)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
2
+ # Do not edit by hand: changes are overwritten. To change this surface,
3
+ # change the API route and its registry entry, run `pnpm sdk:sync`, then
4
+ # `pnpm sdk:generate`.
5
+
6
+ module MisarSEO
7
+ module Generated
8
+ # +client.activity+ — 1 endpoint(s).
9
+ class ActivityResource
10
+ def initialize(transport)
11
+ @transport = transport
12
+ end
13
+
14
+ # GET /activity
15
+ #
16
+ # Optional query parameters: +before+, +pageSize+, +projectId+.
17
+ def list(query: nil)
18
+ @transport.request("GET", "/activity", query: query, body: nil)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ # AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
2
+ # Do not edit by hand: changes are overwritten. To change this surface,
3
+ # change the API route and its registry entry, run `pnpm sdk:sync`, then
4
+ # `pnpm sdk:generate`.
5
+
6
+ module MisarSEO
7
+ module Generated
8
+ # +client.ahrefs+ — 4 endpoint(s).
9
+ class AhrefsResource
10
+ def initialize(transport)
11
+ @transport = transport
12
+ end
13
+
14
+ # GET /ahrefs/connection
15
+ def connect(project_id, query: nil)
16
+ @transport.request("GET", "/ahrefs/connection", query: Transport.merge(query, "projectId" => project_id), body: nil)
17
+ end
18
+
19
+ # DELETE /ahrefs/connection
20
+ def disconnect(body = nil, query: nil, **fields)
21
+ @transport.request("DELETE", "/ahrefs/connection", query: query, body: Transport.body(body, fields))
22
+ end
23
+
24
+ # GET /ahrefs/projects
25
+ def list_projects(project_id, query: nil)
26
+ @transport.request("GET", "/ahrefs/projects", query: Transport.merge(query, "projectId" => project_id), body: nil)
27
+ end
28
+
29
+ # POST /ahrefs/connection
30
+ def set_project(body = nil, query: nil, **fields)
31
+ @transport.request("POST", "/ahrefs/connection", query: query, body: Transport.body(body, fields))
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,87 @@
1
+ # AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
2
+ # Do not edit by hand: changes are overwritten. To change this surface,
3
+ # change the API route and its registry entry, run `pnpm sdk:sync`, then
4
+ # `pnpm sdk:generate`.
5
+
6
+ module MisarSEO
7
+ module Generated
8
+ # +client.ai_search+ — 14 endpoint(s).
9
+ class AiSearchResource
10
+ def initialize(transport)
11
+ @transport = transport
12
+ end
13
+
14
+ # POST /ai/brand-lookup
15
+ def brand_lookup(body = nil, query: nil, **fields)
16
+ @transport.request("POST", "/ai/brand-lookup", query: query, body: Transport.body(body, fields))
17
+ end
18
+
19
+ # GET /ai/brand-lookup/:jobId/status
20
+ def brand_lookup_status(job_id, project_id, query: nil)
21
+ @transport.request("GET", "/ai/brand-lookup/#{Transport.seg(job_id)}/status", query: Transport.merge(query, "projectId" => project_id), body: nil)
22
+ end
23
+
24
+ # GET /ai/brand-scan-trend
25
+ #
26
+ # Optional query parameters: +since+.
27
+ def brand_scan_trend(project_id, target, query: nil)
28
+ @transport.request("GET", "/ai/brand-scan-trend", query: Transport.merge(query, "projectId" => project_id, "target" => target), body: nil)
29
+ end
30
+
31
+ # POST /citations
32
+ def citations(body = nil, query: nil, **fields)
33
+ @transport.request("POST", "/citations", query: query, body: Transport.body(body, fields))
34
+ end
35
+
36
+ # POST /ai/competitor-positioning
37
+ def competitor_positioning(body = nil, query: nil, **fields)
38
+ @transport.request("POST", "/ai/competitor-positioning", query: query, body: Transport.body(body, fields))
39
+ end
40
+
41
+ # DELETE /ai/brand-monitors/:monitorId
42
+ def delete_brand_monitor(monitor_id, project_id, query: nil)
43
+ @transport.request("DELETE", "/ai/brand-monitors/#{Transport.seg(monitor_id)}", query: Transport.merge(query, "projectId" => project_id), body: nil)
44
+ end
45
+
46
+ # DELETE /ai/prompt-tracking/:trackedPromptId
47
+ def delete_tracked_prompt(tracked_prompt_id, project_id, query: nil)
48
+ @transport.request("DELETE", "/ai/prompt-tracking/#{Transport.seg(tracked_prompt_id)}", query: Transport.merge(query, "projectId" => project_id), body: nil)
49
+ end
50
+
51
+ # GET /ai/brand-monitors
52
+ def list_brand_monitors(project_id, query: nil)
53
+ @transport.request("GET", "/ai/brand-monitors", query: Transport.merge(query, "projectId" => project_id), body: nil)
54
+ end
55
+
56
+ # GET /ai/prompt-tracking
57
+ def list_tracked_prompts(project_id, query: nil)
58
+ @transport.request("GET", "/ai/prompt-tracking", query: Transport.merge(query, "projectId" => project_id), body: nil)
59
+ end
60
+
61
+ # POST /ai/prompt-explorer
62
+ def prompt_explorer(body = nil, query: nil, **fields)
63
+ @transport.request("POST", "/ai/prompt-explorer", query: query, body: Transport.body(body, fields))
64
+ end
65
+
66
+ # POST /ai/prompt-research
67
+ def prompt_research(body = nil, query: nil, **fields)
68
+ @transport.request("POST", "/ai/prompt-research", query: query, body: Transport.body(body, fields))
69
+ end
70
+
71
+ # GET /ai/prompt-tracking/:trackedPromptId/trend
72
+ def tracked_prompt_trend(tracked_prompt_id, project_id, query: nil)
73
+ @transport.request("GET", "/ai/prompt-tracking/#{Transport.seg(tracked_prompt_id)}/trend", query: Transport.merge(query, "projectId" => project_id), body: nil)
74
+ end
75
+
76
+ # POST /ai/brand-monitors
77
+ def upsert_brand_monitor(body = nil, query: nil, **fields)
78
+ @transport.request("POST", "/ai/brand-monitors", query: query, body: Transport.body(body, fields))
79
+ end
80
+
81
+ # POST /ai/prompt-tracking
82
+ def upsert_tracked_prompt(body = nil, query: nil, **fields)
83
+ @transport.request("POST", "/ai/prompt-tracking", query: query, body: Transport.body(body, fields))
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,34 @@
1
+ # AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
2
+ # Do not edit by hand: changes are overwritten. To change this surface,
3
+ # change the API route and its registry entry, run `pnpm sdk:sync`, then
4
+ # `pnpm sdk:generate`.
5
+
6
+ module MisarSEO
7
+ module Generated
8
+ # +client.backlinks+ — 3 endpoint(s).
9
+ class BacklinksResource
10
+ def initialize(transport)
11
+ @transport = transport
12
+ end
13
+
14
+ # GET /backlinks/anchors
15
+ #
16
+ # Optional query parameters: +page+, +pageSize+, +scope+, +sortField+, +sortOrder+.
17
+ def anchors(project_id, target, query: nil)
18
+ @transport.request("GET", "/backlinks/anchors", query: Transport.merge(query, "projectId" => project_id, "target" => target), body: nil)
19
+ end
20
+
21
+ # POST /backlinks/intersect
22
+ def intersect(body = nil, query: nil, **fields)
23
+ @transport.request("POST", "/backlinks/intersect", query: query, body: Transport.body(body, fields))
24
+ end
25
+
26
+ # GET /backlinks
27
+ #
28
+ # Optional query parameters: +scope+.
29
+ def overview(project_id, target, query: nil)
30
+ @transport.request("GET", "/backlinks", query: Transport.merge(query, "projectId" => project_id, "target" => target), body: nil)
31
+ end
32
+ end
33
+ end
34
+ end