watchfor 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: 8a768a7411f989ca33ba64fd4a8ea911931a93b9265421c1e12664b140cf7aed
4
+ data.tar.gz: 64381c817f5250cb897219319a6c29d851759f7ce241296e270fc013700207b2
5
+ SHA512:
6
+ metadata.gz: 1604b16cff061ac83c75dbde6ebbe77c9b513c6bf11654f57cb70bc3b8a0007186bded561c02bc6aa4c2a69d2e06e1747864f63d7f9006039549316e3df36644
7
+ data.tar.gz: 4f9e4d9014ba6491f405564bc1224d8377482db9251d0d37cddfe6e8849f5a406c43c3f3c7f80c9f113bca27e44d0fd5ff29c549e2ab805a4fb34f3b82e2c934
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WatchFor
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,55 @@
1
+ # watchfor (Ruby)
2
+
3
+ Official Ruby SDK + CLI for [WatchFor](https://watchfor.io) — uptime &
4
+ infrastructure monitoring over the REST API. Zero dependencies (Ruby stdlib only).
5
+
6
+ Also available: a [TypeScript SDK](https://www.npmjs.com/package/watchfor), a
7
+ [Python SDK](https://pypi.org/project/watchfor/), an
8
+ [MCP server](https://watchfor.io/docs/api/mcp) and an
9
+ [A2A agent](https://watchfor.io/docs/api/a2a).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ gem install watchfor
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ruby
20
+ require "watchfor"
21
+
22
+ wf = Watchfor::Client.new(api_key: "wf_live_...") # Settings → API keys
23
+
24
+ pp wf.summary
25
+
26
+ wf.monitors.list["data"].each { |m| puts "#{m['name']} #{m['status']}" }
27
+
28
+ mon = wf.monitors.create(
29
+ { name: "example.com", type: "http", target: "https://example.com",
30
+ interval: 300, locations: ["<location-id>"] }, # from wf.locations
31
+ idempotency_key: "create-example-1"
32
+ )
33
+
34
+ wf.incidents.list(status: "firing")["data"].each { |i| puts i["message"] }
35
+ ```
36
+
37
+ Namespaces: `wf.monitors`, `wf.alert_rules`, `wf.incidents`,
38
+ `wf.maintenance_windows`, `wf.contacts`, `wf.contact_groups`. Top-level:
39
+ `wf.summary`, `wf.plan`, `wf.me`, `wf.locations`, `wf.monitor_types`,
40
+ `wf.incident_stats(period)`, `wf.notifications`, `wf.activity`. Errors raise
41
+ `Watchfor::Error` with `#status`, `#code`, `#message`.
42
+
43
+ ## CLI
44
+
45
+ ```bash
46
+ export WATCHFOR_API_KEY=wf_live_...
47
+ watchfor summary
48
+ watchfor monitors
49
+ watchfor incidents
50
+ watchfor checks <monitor_id>
51
+ ```
52
+
53
+ Reference: <https://watchfor.io/openapi.json> · <https://watchfor.io/docs/api>
54
+
55
+ MIT License.
data/exe/watchfor ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "watchfor"
6
+
7
+ api_key = ENV["WATCHFOR_API_KEY"]
8
+ base_url = ENV.fetch("WATCHFOR_BASE_URL", Watchfor::DEFAULT_BASE_URL)
9
+
10
+ args = ARGV.dup
11
+ command = args.shift
12
+
13
+ if command == "--version"
14
+ puts "watchfor #{Watchfor::VERSION}"
15
+ exit 0
16
+ end
17
+
18
+ if api_key.nil? || api_key.empty?
19
+ warn "No API key. Set WATCHFOR_API_KEY."
20
+ exit 2
21
+ end
22
+
23
+ wf = Watchfor::Client.new(api_key: api_key, base_url: base_url)
24
+
25
+ begin
26
+ result =
27
+ case command
28
+ when "summary" then wf.summary
29
+ when "plan" then wf.plan
30
+ when "monitors" then wf.monitors.list
31
+ when "locations" then wf.locations
32
+ when "monitor-types" then wf.monitor_types
33
+ when "incidents" then wf.incidents.list
34
+ when "checks" then wf.monitors.checks(args.shift)
35
+ else
36
+ warn "Usage: watchfor [summary|plan|monitors|locations|monitor-types|incidents|checks <monitor_id>]"
37
+ exit 2
38
+ end
39
+ puts JSON.pretty_generate(result)
40
+ rescue Watchfor::Error => e
41
+ warn "error: #{e.message}"
42
+ exit 1
43
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Watchfor
8
+ DEFAULT_BASE_URL = "https://watchfor.io/api/v1"
9
+
10
+ # Raised when the API returns a non-2xx response. Carries the HTTP
11
+ # +status+ and the API's structured +code+ and +message+.
12
+ class Error < StandardError
13
+ attr_reader :status, :code
14
+
15
+ def initialize(status, code, message)
16
+ @status = status
17
+ @code = code
18
+ super("[#{status}] #{code}: #{message}")
19
+ end
20
+ end
21
+
22
+ # Client for the WatchFor REST API (/api/v1). Zero dependencies.
23
+ #
24
+ # wf = Watchfor::Client.new(api_key: "wf_live_...")
25
+ # wf.summary
26
+ # wf.monitors.list["data"].each { |m| puts "#{m['name']} #{m['status']}" }
27
+ class Client
28
+ attr_reader :monitors, :alert_rules, :incidents,
29
+ :maintenance_windows, :contacts, :contact_groups
30
+
31
+ def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: 30)
32
+ raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
33
+
34
+ @api_key = api_key
35
+ @base_url = base_url.chomp("/")
36
+ @timeout = timeout
37
+ @monitors = Monitors.new(self)
38
+ @alert_rules = AlertRules.new(self)
39
+ @incidents = Incidents.new(self)
40
+ @maintenance_windows = MaintenanceWindows.new(self)
41
+ @contacts = Contacts.new(self)
42
+ @contact_groups = ContactGroups.new(self)
43
+ end
44
+
45
+ # Core request. +query+ and +body+ are optional hashes.
46
+ def request(method, path, query: nil, body: nil, idempotency_key: nil)
47
+ uri = URI("#{@base_url}#{path}")
48
+ unless query.nil? || query.empty?
49
+ cleaned = query.reject { |_, v| v.nil? || v == "" }
50
+ uri.query = URI.encode_www_form(cleaned) unless cleaned.empty?
51
+ end
52
+
53
+ klass = {
54
+ "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post,
55
+ "PATCH" => Net::HTTP::Patch, "PUT" => Net::HTTP::Put,
56
+ "DELETE" => Net::HTTP::Delete
57
+ }.fetch(method)
58
+ req = klass.new(uri)
59
+ req["Authorization"] = "Bearer #{@api_key}"
60
+ req["Accept"] = "application/json"
61
+ req["User-Agent"] = "watchfor-ruby/#{Watchfor::VERSION}"
62
+ req["Idempotency-Key"] = idempotency_key if idempotency_key
63
+ unless body.nil?
64
+ req["Content-Type"] = "application/json"
65
+ req.body = JSON.generate(body)
66
+ end
67
+
68
+ res = Net::HTTP.start(uri.hostname, uri.port,
69
+ use_ssl: uri.scheme == "https",
70
+ open_timeout: @timeout, read_timeout: @timeout) do |http|
71
+ http.request(req)
72
+ end
73
+
74
+ raw = res.body.to_s
75
+ parsed = raw.empty? ? nil : (JSON.parse(raw) rescue nil)
76
+ unless res.code.to_i.between?(200, 299)
77
+ err = parsed.is_a?(Hash) ? (parsed["error"] || {}) : {}
78
+ raise Error.new(res.code.to_i, err["code"] || "http_error",
79
+ err["message"] || "HTTP #{res.code}")
80
+ end
81
+ parsed.nil? ? { "ok" => true, "status" => res.code.to_i } : parsed
82
+ end
83
+
84
+ # Top-level reads
85
+ def me = request("GET", "/me")
86
+ def summary = request("GET", "/summary")
87
+ def plan = request("GET", "/plan")
88
+ def incident_stats(period = nil) = request("GET", "/incidents/stats", query: { period: period })
89
+ def locations = request("GET", "/locations")
90
+ def monitor_types = request("GET", "/meta/monitor-types")
91
+ def notifications(**query) = request("GET", "/notifications", query: query)
92
+ def activity(**query) = request("GET", "/activity", query: query)
93
+
94
+ # Namespaces
95
+ class Namespace
96
+ def initialize(client) = (@c = client)
97
+ end
98
+
99
+ class Monitors < Namespace
100
+ def list(**query) = @c.request("GET", "/monitors", query: query)
101
+ def get(id) = @c.request("GET", "/monitors/#{id}")
102
+ def create(body, idempotency_key: nil) = @c.request("POST", "/monitors", body: body, idempotency_key: idempotency_key)
103
+ def update(id, body) = @c.request("PATCH", "/monitors/#{id}", body: body)
104
+ def delete(id) = @c.request("DELETE", "/monitors/#{id}")
105
+ def pause(id) = @c.request("POST", "/monitors/#{id}/pause")
106
+ def resume(id) = @c.request("POST", "/monitors/#{id}/resume")
107
+ def check_now(id) = @c.request("POST", "/monitors/#{id}/check-now")
108
+ def uptime(id, period = nil) = @c.request("GET", "/monitors/#{id}/uptime", query: { period: period })
109
+ def checks(id, **query) = @c.request("GET", "/monitors/#{id}/checks", query: query)
110
+ end
111
+
112
+ class AlertRules < Namespace
113
+ def list(monitor_id) = @c.request("GET", "/monitors/#{monitor_id}/alert-rules")
114
+ def get(monitor_id, rule_id) = @c.request("GET", "/monitors/#{monitor_id}/alert-rules/#{rule_id}")
115
+ def create(monitor_id, body) = @c.request("POST", "/monitors/#{monitor_id}/alert-rules", body: body)
116
+ def update(monitor_id, rule_id, body) = @c.request("PATCH", "/monitors/#{monitor_id}/alert-rules/#{rule_id}", body: body)
117
+ def delete(monitor_id, rule_id) = @c.request("DELETE", "/monitors/#{monitor_id}/alert-rules/#{rule_id}")
118
+ end
119
+
120
+ class Incidents < Namespace
121
+ def list(**query) = @c.request("GET", "/incidents", query: query)
122
+ def get(id) = @c.request("GET", "/incidents/#{id}")
123
+ def acknowledge(id) = @c.request("POST", "/incidents/#{id}/acknowledge")
124
+ def resolve(id) = @c.request("POST", "/incidents/#{id}/resolve")
125
+ end
126
+
127
+ class MaintenanceWindows < Namespace
128
+ def list(**query) = @c.request("GET", "/maintenance-windows", query: query)
129
+ def get(id) = @c.request("GET", "/maintenance-windows/#{id}")
130
+ def create(body, idempotency_key: nil) = @c.request("POST", "/maintenance-windows", body: body, idempotency_key: idempotency_key)
131
+ def update(id, body) = @c.request("PATCH", "/maintenance-windows/#{id}", body: body)
132
+ def delete(id) = @c.request("DELETE", "/maintenance-windows/#{id}")
133
+ end
134
+
135
+ class Contacts < Namespace
136
+ def list(**query) = @c.request("GET", "/contacts", query: query)
137
+ def get(id) = @c.request("GET", "/contacts/#{id}")
138
+ def create(body, idempotency_key: nil) = @c.request("POST", "/contacts", body: body, idempotency_key: idempotency_key)
139
+ def update(id, body) = @c.request("PATCH", "/contacts/#{id}", body: body)
140
+ def delete(id) = @c.request("DELETE", "/contacts/#{id}")
141
+ end
142
+
143
+ class ContactGroups < Namespace
144
+ def list(**query) = @c.request("GET", "/contact-groups", query: query)
145
+ def get(id) = @c.request("GET", "/contact-groups/#{id}")
146
+ def create(body, idempotency_key: nil) = @c.request("POST", "/contact-groups", body: body, idempotency_key: idempotency_key)
147
+ def update(id, body) = @c.request("PATCH", "/contact-groups/#{id}", body: body)
148
+ def delete(id) = @c.request("DELETE", "/contact-groups/#{id}")
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Watchfor
4
+ VERSION = "0.1.0"
5
+ end
data/lib/watchfor.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "watchfor/version"
4
+ require_relative "watchfor/client"
5
+
6
+ # Official Ruby SDK for WatchFor — uptime & infrastructure monitoring.
7
+ # See https://watchfor.io/docs/api
8
+ module Watchfor
9
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: watchfor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - WatchFor
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Zero-dependency Ruby client for the WatchFor REST API (monitors, alert
14
+ rules, incidents, maintenance windows) plus a `watchfor` CLI.
15
+ email:
16
+ - hello@watchfor.io
17
+ executables:
18
+ - watchfor
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE
23
+ - README.md
24
+ - exe/watchfor
25
+ - lib/watchfor.rb
26
+ - lib/watchfor/client.rb
27
+ - lib/watchfor/version.rb
28
+ homepage: https://watchfor.io/docs/api
29
+ licenses:
30
+ - MIT
31
+ metadata:
32
+ homepage_uri: https://watchfor.io/docs/api
33
+ documentation_uri: https://watchfor.io/docs/api
34
+ changelog_uri: https://watchfor.io/changelog
35
+ rubygems_mfa_required: 'true'
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '3.0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.4.20
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Official Ruby SDK + CLI for WatchFor — uptime & infrastructure monitoring.
55
+ test_files: []