activerabbit-cli 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3a37cdfbd12a95561bee8ddd354ef1f2df2c1c913c90a6cc6a9735cb2917c021
4
+ data.tar.gz: 48b2e505b9ef3b2c60444104f8afaf20bfccec8e94414f3f4d4442b32550a11b
5
+ SHA512:
6
+ metadata.gz: 0d278067e8c0ae78af59133ba4d2e633f8eef8e2d3e985be1775fc4f74b70020a3725a9ecd921bf54bb63e7e1d58dee30b837e41b39cb7fe2f2958daf2fc02b2
7
+ data.tar.gz: 7b307c5640e79b88f1c20f46b2bf4dd3c5796d9712a7b872fefe90ae8f02aeba20a3382924899533ccd8ca4fa61e0c107d9f46d33ba7f23d8cc11abf68b651a6
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to activerabbit-cli will be documented in this file.
4
+
5
+ ## [1.0.0] - 2025-02-25
6
+
7
+ ### Added
8
+ - Initial release
9
+ - `activerabbit login` — authenticate with API key
10
+ - `activerabbit apps` — list available apps
11
+ - `activerabbit use-app <slug>` — set default app
12
+ - `activerabbit status` — app health snapshot
13
+ - `activerabbit incidents` — list incidents with colorized severity and relative time
14
+ - `activerabbit show <id>` — incident details with stack trace
15
+ - `activerabbit explain <id>` — AI-powered root cause analysis
16
+ - `activerabbit trace <endpoint>` — trace analysis with visual span bars
17
+ - `activerabbit deploy-check` — pre-deploy safety check
18
+ - `activerabbit doctor` — verify config and connectivity
19
+ - Multi-app support with interactive selection
20
+ - Output formats: human (default), `--json`, `--md`
21
+ - Config file at `~/.config/activerabbit/config.yml`
22
+ - Environment variable support: `ACTIVERABBIT_API_KEY`, `ACTIVERABBIT_APP`, `ACTIVERABBIT_BASE_URL`
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 ActiveRabbit
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # activerabbit-cli
2
+
3
+ Production CLI for [ActiveRabbit.ai](https://activerabbit.ai): Rails/JS monitoring and AI-powered issue analysis.
4
+
5
+ Optimized for:
6
+ - **Developers** — fast terminal output with colors, relative times, visual bars
7
+ - **AI agents** — deterministic `--json` output for scripts and automation
8
+ - **CI/CD** — `--md` output for Slack/GitHub comments
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ gem install activerabbit-cli
14
+ ```
15
+
16
+ This installs the `activerabbit` command.
17
+
18
+ ### From source
19
+
20
+ ```bash
21
+ git clone https://github.com/activerabbit/activerabbit-cli.git
22
+ cd activerabbit-cli
23
+ gem build activerabbit-cli.gemspec
24
+ gem install activerabbit-cli-1.0.0.gem
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```bash
30
+ # 1. Login with your API key (from app.activerabbit.ai → Settings → API)
31
+ activerabbit login --api-key YOUR_API_KEY
32
+
33
+ # 2. List your apps
34
+ activerabbit apps
35
+
36
+ # 3. Select default app
37
+ activerabbit use-app jobsgpt-prod
38
+
39
+ # 4. Check status
40
+ activerabbit status
41
+ ```
42
+
43
+ Or use environment variables:
44
+
45
+ ```bash
46
+ export ACTIVERABBIT_API_KEY=your_token
47
+ export ACTIVERABBIT_APP=your-app-slug
48
+ activerabbit incidents
49
+ ```
50
+
51
+ ## Commands
52
+
53
+ | Command | Description |
54
+ |--------|-------------|
55
+ | `activerabbit login` | Authenticate with API key |
56
+ | `activerabbit apps` | List available apps |
57
+ | `activerabbit use-app <slug>` | Set default app |
58
+ | `activerabbit status` | App health: errors (24h), p95 latency, deploy status, top issue |
59
+ | `activerabbit incidents [--limit N]` | List incidents with severity, endpoint, count, relative time |
60
+ | `activerabbit show <id>` | Incident details: stack trace, affected users, recent events |
61
+ | `activerabbit explain <id>` | AI analysis: root cause, suggested fix, regression risk, tests |
62
+ | `activerabbit trace <endpoint\|id>` | Trace analysis with visual span bars and bottlenecks |
63
+ | `activerabbit deploy-check` | Pre-deploy safety check |
64
+ | `activerabbit doctor` | Verify config and API connectivity |
65
+
66
+ ## Output formats
67
+
68
+ Every command supports three output modes:
69
+
70
+ | Flag | Use case |
71
+ |------|----------|
72
+ | *(default)* | Human-readable with colors, relative times, visual bars |
73
+ | `--json` | Deterministic JSON for scripts, CI, AI agents |
74
+ | `--md` | Markdown for Slack, GitHub comments, reports |
75
+
76
+ **Note:** Global options must come **before** the command:
77
+
78
+ ```bash
79
+ # Correct
80
+ activerabbit --json incidents
81
+
82
+ # Also works (command-specific flags after)
83
+ activerabbit --json incidents --limit 5
84
+ ```
85
+
86
+ ## JSON shape
87
+
88
+ All commands return an envelope like:
89
+
90
+ ```json
91
+ {
92
+ "project": "project_id",
93
+ "generated_at": "2025-01-15T12:00:00Z",
94
+ "command": "explain",
95
+ "data": { ... }
96
+ }
97
+ ```
98
+
99
+ Example for `explain`:
100
+
101
+ ```json
102
+ {
103
+ "project": "42",
104
+ "generated_at": "2025-01-15T12:00:00Z",
105
+ "command": "explain",
106
+ "data": {
107
+ "incident_id": "inc_123",
108
+ "severity": "high",
109
+ "title": "NoMethodError in JobsController#show",
110
+ "root_cause": "...",
111
+ "confidence_score": 0.87,
112
+ "affected_endpoints": ["/jobs", "/jobs/:id"],
113
+ "suggested_fix": "...",
114
+ "regression_risk": "medium",
115
+ "tests_to_run": ["spec/requests/jobs_spec.rb"],
116
+ "estimated_impact": "..."
117
+ }
118
+ }
119
+ ```
120
+
121
+ ## CI / script example
122
+
123
+ ```bash
124
+ # Fail deploy if project not healthy
125
+ activerabbit status --json | jq -e '.data.health == "ok"' || exit 1
126
+
127
+ # Post incident summary to Slack (e.g. from GitHub Actions)
128
+ activerabbit explain "$INCIDENT_ID" --md > summary.md
129
+ # Then upload summary.md to Slack
130
+ ```
131
+
132
+ ## Configuration
133
+
134
+ Config priority (highest to lowest):
135
+
136
+ 1. CLI flags (`--app`, `--base-url`)
137
+ 2. Environment variables
138
+ 3. Config file (`~/.config/activerabbit/config.yml`)
139
+
140
+ | Env var | Description |
141
+ |---------|-------------|
142
+ | `ACTIVERABBIT_API_KEY` | API token (required) |
143
+ | `ACTIVERABBIT_APP` | Default app slug |
144
+ | `ACTIVERABBIT_BASE_URL` | API URL (default: https://app.activerabbit.ai) |
145
+
146
+ ## CI/CD example
147
+
148
+ ```bash
149
+ #!/bin/bash
150
+ # Deploy gate: fail if app has critical errors
151
+
152
+ status=$(activerabbit --json status)
153
+ health=$(echo "$status" | jq -r '.data.health')
154
+
155
+ if [ "$health" != "ok" ]; then
156
+ echo "Deploy blocked: app health is $health"
157
+ activerabbit incidents --limit 3
158
+ exit 1
159
+ fi
160
+
161
+ echo "App healthy, proceeding with deploy"
162
+ ```
163
+
164
+ ## Roadmap (v2)
165
+
166
+ - GitHub PR integration — auto-comment with explain/deploy-check
167
+ - Deploy diff — compare errors before/after deploy
168
+ - MCP server — expose CLI to AI agents via Model Context Protocol
169
+ - `activerabbit watch` — stream incidents in real-time
170
+ - Webhook triggers — run CLI commands on new incidents
data/exe/activerabbit ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "activerabbit"
5
+
6
+ ActiveRabbit::CLI.new(ARGV).run
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require "time"
7
+
8
+ module ActiveRabbit
9
+ # HTTP client for ActiveRabbit API. Uses X-Project-Token (project API key).
10
+ # Designed so backend can add real endpoints (status, incidents, explain, traces) later;
11
+ # until then, uses mock responses when server returns 404 or endpoint is missing.
12
+ class ApiClient
13
+ DEFAULT_BASE_URL = "https://app.activerabbit.ai"
14
+ OPEN_TIMEOUT = 10
15
+ READ_TIMEOUT = 30
16
+
17
+ class Error < StandardError; end
18
+ class Unauthorized < Error; end
19
+ class NotFound < Error; end
20
+ class RateLimited < Error; end
21
+ class NetworkError < Error; end
22
+
23
+ def initialize(config)
24
+ @config = config
25
+ @base_url = (config.base_url.to_s.strip != "" ? config.base_url : DEFAULT_BASE_URL).to_s
26
+ end
27
+
28
+ def get(path, query = {})
29
+ uri = build_uri(path, query)
30
+ req = Net::HTTP::Get.new(uri)
31
+ request(uri, req)
32
+ end
33
+
34
+ def post(path, body = {})
35
+ uri = build_uri(path)
36
+ req = Net::HTTP::Post.new(uri)
37
+ req["Content-Type"] = "application/json"
38
+ req.body = body.is_a?(String) ? body : body.to_json
39
+ request(uri, req)
40
+ end
41
+
42
+ # --- API methods (return hashes; mock when backend not ready) ---
43
+
44
+ def list_apps
45
+ get("/api/v1/cli/apps")
46
+ end
47
+
48
+ def project_status(app = nil)
49
+ id = app || @config.effective_app
50
+ get("/api/v1/cli/apps/#{id}/status")
51
+ end
52
+
53
+ def incidents(app = nil, limit: 10)
54
+ id = app || @config.effective_app
55
+ get("/api/v1/cli/apps/#{id}/incidents", "limit" => limit)
56
+ end
57
+
58
+ def incident_detail(incident_id, app = nil)
59
+ id = app || @config.effective_app
60
+ get("/api/v1/cli/apps/#{id}/incidents/#{incident_id}")
61
+ end
62
+
63
+ def explain_incident(incident_id, app = nil)
64
+ id = app || @config.effective_app
65
+ get("/api/v1/cli/apps/#{id}/incidents/#{incident_id}/explain")
66
+ end
67
+
68
+ def trace(endpoint_or_trace_id, app = nil)
69
+ id = app || @config.effective_app
70
+ if endpoint_or_trace_id.include?("/") && !endpoint_or_trace_id.start_with?("tr_")
71
+ get("/api/v1/cli/apps/#{id}/traces", "endpoint" => endpoint_or_trace_id)
72
+ else
73
+ get("/api/v1/cli/apps/#{id}/traces/#{endpoint_or_trace_id}")
74
+ end
75
+ end
76
+
77
+ def deploy_check(app = nil)
78
+ id = app || @config.effective_app
79
+ get("/api/v1/cli/apps/#{id}/deploy_check")
80
+ end
81
+
82
+ private
83
+
84
+ def build_uri(path, query = {})
85
+ full = path.start_with?("http") ? path : "#{@base_url}#{path}"
86
+ uri = URI(full)
87
+ uri.query = URI.encode_www_form(query) if query.any?
88
+ uri
89
+ end
90
+
91
+ def request(uri, req)
92
+ req["X-Project-Token"] = @config.api_key
93
+ req["Accept"] = "application/json"
94
+ req["User-Agent"] = "activerabbit-cli/#{ActiveRabbit::VERSION}"
95
+
96
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT) do |http|
97
+ http.request(req)
98
+ end
99
+
100
+ handle_response(uri, res)
101
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError, Net::OpenTimeout, Net::ReadTimeout => e
102
+ raise NetworkError, "Network error: #{e.message}"
103
+ end
104
+
105
+ def handle_response(uri, res)
106
+ body = res.body.to_s.strip
107
+ parsed = (body.empty? ? {} : JSON.parse(body)) rescue {}
108
+
109
+ case res.code.to_i
110
+ when 200, 201
111
+ parsed.is_a?(Hash) ? parsed : { "data" => parsed }
112
+ when 401
113
+ raise Unauthorized, parsed["message"] || "Invalid or missing API key"
114
+ when 404
115
+ # Return mock so CLI works before backend has CLI endpoints
116
+ mock_for(uri, res: res, parsed: parsed)
117
+ when 429
118
+ raise RateLimited, parsed["message"] || "Too many requests"
119
+ when 500..599
120
+ raise Error, parsed["message"] || "Server error (#{res.code})"
121
+ else
122
+ raise Error, parsed["message"] || "Request failed (#{res.code})"
123
+ end
124
+ end
125
+
126
+ def mock_for(uri, res:, parsed:)
127
+ path = uri.path
128
+ query = URI.decode_www_form(uri.query || "").to_h
129
+ if path =~ %r{/cli/apps$}
130
+ mock_apps
131
+ elsif path.include?("/status")
132
+ mock_status
133
+ elsif path.include?("/incidents") && !path.include?("/explain")
134
+ if path =~ %r{/incidents/([^/]+)$}
135
+ mock_incident_detail($1)
136
+ else
137
+ mock_incidents
138
+ end
139
+ elsif path.include?("/explain")
140
+ mock_explain(path)
141
+ elsif path.include?("/traces")
142
+ mock_trace(path, query)
143
+ elsif path.include?("/deploy_check")
144
+ mock_deploy_check
145
+ else
146
+ raise NotFound, parsed["message"] || "Not found"
147
+ end
148
+ end
149
+
150
+ def mock_apps
151
+ {
152
+ "generated_at" => Time.now.utc.iso8601,
153
+ "command" => "apps",
154
+ "data" => {
155
+ "apps" => [
156
+ { "slug" => "jobsgpt-prod", "name" => "JobsGPT Production", "environment" => "production", "error_count_24h" => 12 },
157
+ { "slug" => "activerabbit-marketing", "name" => "ActiveRabbit Marketing", "environment" => "production", "error_count_24h" => 0 },
158
+ { "slug" => "client-storefront", "name" => "Client Storefront", "environment" => "staging", "error_count_24h" => 3 }
159
+ ]
160
+ }
161
+ }
162
+ end
163
+
164
+ def mock_status
165
+ app = @config.effective_app || "default"
166
+ {
167
+ "project" => app,
168
+ "generated_at" => Time.now.utc.iso8601,
169
+ "command" => "status",
170
+ "data" => {
171
+ "app" => app,
172
+ "name" => "JobsGPT Production",
173
+ "health" => "ok",
174
+ "error_count_24h" => 12,
175
+ "p95_latency_ms" => 245,
176
+ "deploy_status" => "stable",
177
+ "last_deploy_at" => (Time.now - 3600).utc.iso8601,
178
+ "top_issue" => {
179
+ "id" => "inc_abc123",
180
+ "title" => "NoMethodError in JobsController#show",
181
+ "count" => 8,
182
+ "severity" => "high"
183
+ }
184
+ }
185
+ }
186
+ end
187
+
188
+ def mock_incidents
189
+ app = @config.effective_app || "default"
190
+ now = Time.now
191
+ {
192
+ "project" => app,
193
+ "generated_at" => now.utc.iso8601,
194
+ "command" => "incidents",
195
+ "data" => {
196
+ "incidents" => [
197
+ { "id" => "inc_abc123", "severity" => "high", "title" => "NoMethodError in JobsController#show", "endpoint" => "GET /jobs/:id", "count" => 127, "last_seen_at" => now.utc.iso8601, "first_seen_at" => (now - 86400).utc.iso8601, "status" => "open" },
198
+ { "id" => "inc_def456", "severity" => "medium", "title" => "ActiveRecord::RecordNotFound in UsersController#show", "endpoint" => "GET /users/:id", "count" => 42, "last_seen_at" => (now - 1800).utc.iso8601, "first_seen_at" => (now - 172800).utc.iso8601, "status" => "open" },
199
+ { "id" => "inc_ghi789", "severity" => "low", "title" => "Timeout::Error in ReportsController#export", "endpoint" => "POST /reports/export", "count" => 8, "last_seen_at" => (now - 7200).utc.iso8601, "first_seen_at" => (now - 259200).utc.iso8601, "status" => "wip" },
200
+ { "id" => "inc_jkl012", "severity" => "critical", "title" => "PG::ConnectionBad in ApplicationController", "endpoint" => "GET /dashboard", "count" => 3, "last_seen_at" => (now - 300).utc.iso8601, "first_seen_at" => (now - 600).utc.iso8601, "status" => "open" }
201
+ ]
202
+ }
203
+ }
204
+ end
205
+
206
+ def mock_incident_detail(incident_id)
207
+ app = @config.effective_app || "default"
208
+ now = Time.now
209
+ {
210
+ "project" => app,
211
+ "generated_at" => now.utc.iso8601,
212
+ "command" => "incident_detail",
213
+ "data" => {
214
+ "id" => incident_id,
215
+ "severity" => "high",
216
+ "status" => "open",
217
+ "title" => "NoMethodError in JobsController#show",
218
+ "exception_class" => "NoMethodError",
219
+ "message" => "undefined method `status' for nil:NilClass",
220
+ "endpoint" => "GET /jobs/:id",
221
+ "count" => 127,
222
+ "affected_users" => 45,
223
+ "first_seen_at" => (now - 86400).utc.iso8601,
224
+ "last_seen_at" => now.utc.iso8601,
225
+ "top_frame" => "app/controllers/jobs_controller.rb:42:in `show'",
226
+ "backtrace" => [
227
+ "app/controllers/jobs_controller.rb:42:in `show'",
228
+ "app/controllers/application_controller.rb:15:in `wrap_request'",
229
+ "config/initializers/activerabbit.rb:8:in `block in <main>'"
230
+ ],
231
+ "recent_events" => [
232
+ { "at" => now.utc.iso8601, "user_id" => "user_123", "request_id" => "req_abc" },
233
+ { "at" => (now - 60).utc.iso8601, "user_id" => "user_456", "request_id" => "req_def" },
234
+ { "at" => (now - 180).utc.iso8601, "user_id" => "user_789", "request_id" => "req_ghi" }
235
+ ],
236
+ "tags" => { "controller" => "JobsController", "action" => "show", "environment" => "production" }
237
+ }
238
+ }
239
+ end
240
+
241
+ def mock_explain(path)
242
+ parts = path.split("/")
243
+ idx = parts.index("incidents")
244
+ incident_id = idx ? parts[idx + 1] : "inc_unknown"
245
+ {
246
+ "project" => @config.project_id || "default",
247
+ "generated_at" => Time.now.utc.iso8601,
248
+ "command" => "explain",
249
+ "data" => {
250
+ "incident_id" => incident_id,
251
+ "severity" => "high",
252
+ "title" => "NoMethodError in JobsController#show",
253
+ "root_cause" => "Call to #status on nil when job was deleted or ID invalid.",
254
+ "confidence_score" => 0.87,
255
+ "affected_endpoints" => ["/jobs", "/jobs/:id"],
256
+ "suggested_fix" => "Add a guard: return head :not_found unless @job",
257
+ "regression_risk" => "medium",
258
+ "tests_to_run" => ["spec/requests/jobs_spec.rb", "spec/controllers/jobs_controller_spec.rb"],
259
+ "estimated_impact" => "Low; fix is localized to one action."
260
+ }
261
+ }
262
+ end
263
+
264
+ def mock_trace(path, query = {})
265
+ path_id = path.split("/").last
266
+ endpoint = query["endpoint"]
267
+ if endpoint.to_s.strip != ""
268
+ trace_id = "tr_mock"
269
+ else
270
+ trace_id = path_id
271
+ endpoint = path_id.start_with?("tr_") ? "/jobs" : path_id
272
+ end
273
+ endpoint = "/jobs" if endpoint.to_s.strip == ""
274
+ {
275
+ "project" => @config.project_id || "default",
276
+ "generated_at" => Time.now.utc.iso8601,
277
+ "command" => "trace",
278
+ "data" => {
279
+ "trace_id" => trace_id,
280
+ "endpoint" => endpoint,
281
+ "duration_ms" => 1250,
282
+ "spans" => [
283
+ { "name" => "JobsController#index", "duration_ms" => 1200, "percent" => 96 },
284
+ { "name" => "Job.load_all", "duration_ms" => 980, "percent" => 78 },
285
+ { "name" => "N+1 detected (job.user)", "duration_ms" => 450, "percent" => 36 }
286
+ ],
287
+ "bottlenecks" => ["N+1 on job.user", "Heavy serialization in index.json.jbuilder"]
288
+ }
289
+ }
290
+ end
291
+
292
+ def mock_deploy_check
293
+ {
294
+ "project" => @config.project_id || "default",
295
+ "generated_at" => Time.now.utc.iso8601,
296
+ "command" => "deploy_check",
297
+ "data" => {
298
+ "ready" => true,
299
+ "last_deploy_at" => (Time.now - 3600).utc.iso8601,
300
+ "new_errors_since_deploy" => 0,
301
+ "warnings" => []
302
+ }
303
+ }
304
+ end
305
+ end
306
+ end