rails-informant 0.3.0 โ 0.3.1
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 +4 -4
- data/README.md +6 -9
- data/lib/generators/rails_informant/skill/templates/informant-alerts.sh +40 -0
- data/lib/generators/rails_informant/skill_generator.rb +47 -1
- data/lib/rails_informant/mcp/configuration.rb +3 -46
- data/lib/rails_informant/middleware/error_capture.rb +5 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9190270b29cd61133c36e5083527afb433de74d02bde6a854c07ba0ee525686d
|
|
4
|
+
data.tar.gz: c9240093202eb13ac1fd85993ead95983abcced394efe40a0f49ad5cbe183f7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 285bebc2912dfda97c99474e5a272a51c0959e7aea2c5739b051ccc3e661da69fca77960472d73e5acfc0d55d3cc5e7c8c56a4b48dd2a032abb0010150b8d2a5
|
|
7
|
+
data.tar.gz: 7e11cbeeba65c6e6735c28f3c9b2262a71a8c7a6991d6c5cc31679d2a8062fde7c825e79ea2ca07894328eccc78d58f441c57f2f7d1ebb5c447c573010c4b779
|
data/README.md
CHANGED
|
@@ -132,16 +132,13 @@ The bundled `informant-mcp` executable connects Claude Code to your error data v
|
|
|
132
132
|
|
|
133
133
|
The `rails_informant:skill` generator creates `.mcp.json` automatically. Set `INFORMANT_PRODUCTION_URL` and `INFORMANT_PRODUCTION_TOKEN` as environment variables (e.g., via `.envrc` + direnv). The MCP server inherits env vars from your shell.
|
|
134
134
|
|
|
135
|
-
For multi-environment setups,
|
|
135
|
+
For multi-environment setups, add env vars for each environment:
|
|
136
136
|
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
staging:
|
|
143
|
-
url: https://staging.myapp.com
|
|
144
|
-
token: ${INFORMANT_STAGING_TOKEN}
|
|
137
|
+
```bash
|
|
138
|
+
export INFORMANT_PRODUCTION_URL=https://myapp.com
|
|
139
|
+
export INFORMANT_PRODUCTION_TOKEN=<token>
|
|
140
|
+
export INFORMANT_STAGING_URL=https://staging.myapp.com
|
|
141
|
+
export INFORMANT_STAGING_TOKEN=<token>
|
|
145
142
|
```
|
|
146
143
|
|
|
147
144
|
### Tools
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Informant: Check for unresolved production errors on Claude Code startup.
|
|
3
|
+
# Requires: curl, jq
|
|
4
|
+
# Env vars: INFORMANT_PRODUCTION_URL, INFORMANT_PRODUCTION_TOKEN
|
|
5
|
+
# INFORMANT_PRODUCTION_PATH_PREFIX (optional, default: /informant)
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# Silent exit if env vars are missing or URL is not HTTPS
|
|
10
|
+
[[ -z "${INFORMANT_PRODUCTION_URL:-}" ]] && exit 0
|
|
11
|
+
[[ -z "${INFORMANT_PRODUCTION_TOKEN:-}" ]] && exit 0
|
|
12
|
+
[[ "$INFORMANT_PRODUCTION_URL" == https://* ]] || exit 0
|
|
13
|
+
|
|
14
|
+
# Silent exit if jq is not installed
|
|
15
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
16
|
+
|
|
17
|
+
path_prefix="${INFORMANT_PRODUCTION_PATH_PREFIX:-/informant}"
|
|
18
|
+
url="${INFORMANT_PRODUCTION_URL}${path_prefix}/api/v1/status"
|
|
19
|
+
|
|
20
|
+
# Fetch status (silent on failure)
|
|
21
|
+
response=$(curl -s -f \
|
|
22
|
+
--connect-timeout 3 \
|
|
23
|
+
--max-time 5 \
|
|
24
|
+
-H @- \
|
|
25
|
+
"$url" <<< "Authorization: Bearer ${INFORMANT_PRODUCTION_TOKEN}" \
|
|
26
|
+
2>/dev/null) || exit 0
|
|
27
|
+
|
|
28
|
+
# Parse unresolved count
|
|
29
|
+
unresolved=$(echo "$response" | jq -r '.unresolved_count // 0') || exit 0
|
|
30
|
+
[[ "$unresolved" -eq 0 ]] && exit 0
|
|
31
|
+
|
|
32
|
+
# Format error summary
|
|
33
|
+
label="error"
|
|
34
|
+
[[ "$unresolved" -gt 1 ]] && label="errors"
|
|
35
|
+
echo "๐จ Informant: ${unresolved} unresolved ${label} in production"
|
|
36
|
+
|
|
37
|
+
echo "$response" | jq -r '
|
|
38
|
+
.top_errors[]? |
|
|
39
|
+
" - \(.error_class) (\(.total_occurrences) \(if .total_occurrences == 1 then "occurrence" else "occurrences" end))"
|
|
40
|
+
' 2>/dev/null || true
|
|
@@ -9,6 +9,11 @@ module RailsInformant
|
|
|
9
9
|
copy_file "SKILL.md", ".claude/skills/informant/SKILL.md"
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
def copy_hook_script
|
|
13
|
+
copy_file "informant-alerts.sh", ".claude/hooks/informant-alerts.sh"
|
|
14
|
+
chmod ".claude/hooks/informant-alerts.sh", 0o755
|
|
15
|
+
end
|
|
16
|
+
|
|
12
17
|
def create_or_update_mcp_json
|
|
13
18
|
mcp_path = File.join(destination_root, ".mcp.json")
|
|
14
19
|
informant_entry = { "command" => "informant-mcp" }
|
|
@@ -27,14 +32,43 @@ module RailsInformant
|
|
|
27
32
|
say "Could not parse existing .mcp.json โ skipping merge. Add the informant server manually.", :red
|
|
28
33
|
end
|
|
29
34
|
|
|
35
|
+
def create_or_update_settings_json
|
|
36
|
+
settings_path = File.join(destination_root, ".claude", "settings.json")
|
|
37
|
+
hook_command = ".claude/hooks/informant-alerts.sh"
|
|
38
|
+
|
|
39
|
+
if File.exist?(settings_path)
|
|
40
|
+
existing = JSON.parse(File.read(settings_path))
|
|
41
|
+
existing["hooks"] ||= {}
|
|
42
|
+
existing["hooks"]["SessionStart"] ||= []
|
|
43
|
+
|
|
44
|
+
already_registered = existing["hooks"]["SessionStart"].any? do |entry|
|
|
45
|
+
entry["hooks"]&.any? { it["command"] == hook_command }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless already_registered
|
|
49
|
+
existing["hooks"]["SessionStart"] << session_start_hook(hook_command)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
create_file ".claude/settings.json", JSON.pretty_generate(existing) + "\n", force: true
|
|
53
|
+
else
|
|
54
|
+
create_file ".claude/settings.json", JSON.pretty_generate(
|
|
55
|
+
"hooks" => { "SessionStart" => [ session_start_hook(hook_command) ] }
|
|
56
|
+
) + "\n"
|
|
57
|
+
end
|
|
58
|
+
rescue JSON::ParserError
|
|
59
|
+
say "Could not parse existing .claude/settings.json โ skipping hook setup.", :red
|
|
60
|
+
end
|
|
61
|
+
|
|
30
62
|
def print_next_steps
|
|
31
63
|
say ""
|
|
32
64
|
say "Claude Code integration installed!", :green
|
|
33
65
|
say ""
|
|
34
66
|
say " Created .mcp.json"
|
|
35
67
|
say " Created .claude/skills/informant/SKILL.md"
|
|
68
|
+
say " Created .claude/hooks/informant-alerts.sh"
|
|
69
|
+
say " Created .claude/settings.json (SessionStart hook)"
|
|
36
70
|
say ""
|
|
37
|
-
say "Next step โ set env vars so the MCP server can reach your app.", :yellow
|
|
71
|
+
say "Next step โ set env vars so the MCP server and startup alerts can reach your app.", :yellow
|
|
38
72
|
say "Add to your .envrc (or export manually):"
|
|
39
73
|
say ""
|
|
40
74
|
say " export INFORMANT_PRODUCTION_URL=https://your-app.com"
|
|
@@ -43,6 +77,18 @@ module RailsInformant
|
|
|
43
77
|
say "The token must match rails_informant.api_token in your Rails credentials."
|
|
44
78
|
say "Add .envrc to .gitignore โ it contains secrets."
|
|
45
79
|
say ""
|
|
80
|
+
say "Optional: install jq for startup error alerts (brew install jq)", :cyan
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def session_start_hook(command)
|
|
86
|
+
{
|
|
87
|
+
"matcher" => "startup",
|
|
88
|
+
"hooks" => [
|
|
89
|
+
{ "type" => "command", "command" => command, "timeout" => 10 }
|
|
90
|
+
]
|
|
91
|
+
}
|
|
46
92
|
end
|
|
47
93
|
end
|
|
48
94
|
end
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
1
|
module RailsInformant
|
|
4
2
|
module Mcp
|
|
5
3
|
class Configuration
|
|
6
|
-
CONFIG_PATH = File.expand_path("~/.config/informant-mcp.yml").freeze
|
|
7
|
-
|
|
8
|
-
attr_reader :allow_insecure
|
|
9
|
-
|
|
10
4
|
def initialize(allow_insecure: false)
|
|
11
5
|
@allow_insecure = allow_insecure
|
|
12
|
-
@environments =
|
|
6
|
+
@environments = load_from_env_vars
|
|
13
7
|
@clients = {}
|
|
8
|
+
|
|
9
|
+
raise "No environments configured. Set INFORMANT_<ENV>_URL and INFORMANT_<ENV>_TOKEN environment variables." if @environments.empty?
|
|
14
10
|
end
|
|
15
11
|
|
|
16
12
|
def default_environment
|
|
@@ -34,12 +30,6 @@ module RailsInformant
|
|
|
34
30
|
|
|
35
31
|
private
|
|
36
32
|
|
|
37
|
-
def load_environments
|
|
38
|
-
envs = load_from_yaml.merge(load_from_env_vars)
|
|
39
|
-
raise "No environments configured. Set INFORMANT_<ENV>_URL and INFORMANT_<ENV>_TOKEN environment variables, or create ~/.config/informant-mcp.yml" if envs.empty?
|
|
40
|
-
envs
|
|
41
|
-
end
|
|
42
|
-
|
|
43
33
|
def load_from_env_vars
|
|
44
34
|
ENV.each_with_object({}) do |(key, _), envs|
|
|
45
35
|
next unless (match = key.match(/\AINFORMANT_(.+)_URL\z/))
|
|
@@ -52,39 +42,6 @@ module RailsInformant
|
|
|
52
42
|
}
|
|
53
43
|
end
|
|
54
44
|
end
|
|
55
|
-
|
|
56
|
-
def load_from_yaml
|
|
57
|
-
path = CONFIG_PATH
|
|
58
|
-
return {} unless File.exist?(path)
|
|
59
|
-
|
|
60
|
-
reject_insecure_permissions(path)
|
|
61
|
-
|
|
62
|
-
yaml = YAML.safe_load_file(path)
|
|
63
|
-
return {} unless yaml.is_a?(Hash) && yaml["environments"].is_a?(Hash)
|
|
64
|
-
|
|
65
|
-
yaml["environments"].each_with_object({}) do |(name, config), envs|
|
|
66
|
-
next unless config.is_a?(Hash)
|
|
67
|
-
envs[name] = { path_prefix: config["path_prefix"], token: interpolate(config["token"]), url: config["url"] }
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def reject_insecure_permissions(path)
|
|
72
|
-
mode = File.stat(path).mode
|
|
73
|
-
return unless mode & 0o077 != 0
|
|
74
|
-
|
|
75
|
-
message = "#{path} has insecure permissions (#{format('%04o', mode & 0o7777)}). Run: chmod 600 #{path}"
|
|
76
|
-
|
|
77
|
-
if @allow_insecure
|
|
78
|
-
warn "[RailsInformant] WARNING: #{message}"
|
|
79
|
-
else
|
|
80
|
-
raise "[RailsInformant] #{message} (use --allow-insecure to override)"
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def interpolate(value)
|
|
85
|
-
return value unless value.is_a?(String)
|
|
86
|
-
value.gsub(/\$\{(INFORMANT_\w+)\}/) { ENV[$1] || "" }
|
|
87
|
-
end
|
|
88
45
|
end
|
|
89
46
|
end
|
|
90
47
|
end
|
|
@@ -6,9 +6,11 @@ module RailsInformant
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def call(env)
|
|
9
|
-
@app.call(env).tap do
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
@app.call(env).tap do |status, _headers, _body|
|
|
10
|
+
# Only record rescued exceptions that resulted in a server error.
|
|
11
|
+
# 4xx responses are expected application errors handled by ShowExceptions.
|
|
12
|
+
if (exception = env["rails_informant.rescued_exception"]) && status.to_i >= 500
|
|
13
|
+
record_exception exception, env:
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
rescue StandardError => exception
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-informant
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Lรณpez Prat
|
|
@@ -127,6 +127,7 @@ files:
|
|
|
127
127
|
- exe/informant-mcp
|
|
128
128
|
- lib/generators/rails_informant/install_generator.rb
|
|
129
129
|
- lib/generators/rails_informant/skill/templates/SKILL.md
|
|
130
|
+
- lib/generators/rails_informant/skill/templates/informant-alerts.sh
|
|
130
131
|
- lib/generators/rails_informant/skill_generator.rb
|
|
131
132
|
- lib/generators/rails_informant/templates/create_informant_tables.rb.erb
|
|
132
133
|
- lib/generators/rails_informant/templates/initializer.rb.erb
|