source_monitor 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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/sm-architecture/SKILL.md +233 -0
  3. data/.claude/skills/sm-architecture/reference/extraction-patterns.md +192 -0
  4. data/.claude/skills/sm-architecture/reference/module-map.md +194 -0
  5. data/.claude/skills/sm-configuration-setting/SKILL.md +264 -0
  6. data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +248 -0
  7. data/.claude/skills/sm-configuration-setting/reference/settings-pattern.md +297 -0
  8. data/.claude/skills/sm-configure/SKILL.md +153 -0
  9. data/.claude/skills/sm-configure/reference/configuration-reference.md +321 -0
  10. data/.claude/skills/sm-dashboard-widget/SKILL.md +344 -0
  11. data/.claude/skills/sm-dashboard-widget/reference/dashboard-patterns.md +304 -0
  12. data/.claude/skills/sm-domain-model/SKILL.md +188 -0
  13. data/.claude/skills/sm-domain-model/reference/model-graph.md +114 -0
  14. data/.claude/skills/sm-domain-model/reference/table-structure.md +348 -0
  15. data/.claude/skills/sm-engine-migration/SKILL.md +395 -0
  16. data/.claude/skills/sm-engine-migration/reference/migration-conventions.md +255 -0
  17. data/.claude/skills/sm-engine-test/SKILL.md +302 -0
  18. data/.claude/skills/sm-engine-test/reference/test-helpers.md +259 -0
  19. data/.claude/skills/sm-engine-test/reference/test-patterns.md +411 -0
  20. data/.claude/skills/sm-event-handler/SKILL.md +265 -0
  21. data/.claude/skills/sm-event-handler/reference/events-api.md +229 -0
  22. data/.claude/skills/sm-health-rule/SKILL.md +327 -0
  23. data/.claude/skills/sm-health-rule/reference/health-system.md +269 -0
  24. data/.claude/skills/sm-host-setup/SKILL.md +223 -0
  25. data/.claude/skills/sm-host-setup/reference/initializer-template.md +195 -0
  26. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +134 -0
  27. data/.claude/skills/sm-job/SKILL.md +263 -0
  28. data/.claude/skills/sm-job/reference/job-conventions.md +245 -0
  29. data/.claude/skills/sm-model-extension/SKILL.md +287 -0
  30. data/.claude/skills/sm-model-extension/reference/extension-api.md +317 -0
  31. data/.claude/skills/sm-pipeline-stage/SKILL.md +254 -0
  32. data/.claude/skills/sm-pipeline-stage/reference/completion-handlers.md +152 -0
  33. data/.claude/skills/sm-pipeline-stage/reference/entry-processing.md +191 -0
  34. data/.claude/skills/sm-pipeline-stage/reference/feed-fetcher-architecture.md +198 -0
  35. data/.claude/skills/sm-scraper-adapter/SKILL.md +284 -0
  36. data/.claude/skills/sm-scraper-adapter/reference/adapter-contract.md +167 -0
  37. data/.claude/skills/sm-scraper-adapter/reference/example-adapter.md +274 -0
  38. data/.vbw-planning/.notification-log.jsonl +54 -0
  39. data/.vbw-planning/.session-log.jsonl +121 -0
  40. data/CHANGELOG.md +9 -0
  41. data/CLAUDE.md +43 -0
  42. data/Gemfile.lock +20 -21
  43. data/lib/source_monitor/setup/workflow.rb +17 -2
  44. data/lib/source_monitor/version.rb +1 -1
  45. data/lib/tasks/source_monitor_setup.rake +58 -0
  46. data/source_monitor.gemspec +1 -0
  47. metadata +37 -1
@@ -0,0 +1,274 @@
1
+ # Example Scraper Adapter
2
+
3
+ A complete working example of a custom scraper adapter.
4
+
5
+ ## Use Case
6
+
7
+ This adapter extracts content from pages that require API-based rendering (e.g., JavaScript-heavy sites that need a headless browser service).
8
+
9
+ ## Implementation
10
+
11
+ ```ruby
12
+ # app/scrapers/my_app/scrapers/headless.rb
13
+ module MyApp
14
+ module Scrapers
15
+ class Headless < SourceMonitor::Scrapers::Base
16
+ # Default settings for this adapter.
17
+ # Overridable per-source via source.scrape_settings JSON column,
18
+ # or per-invocation via the settings parameter.
19
+ def self.default_settings
20
+ {
21
+ render_service_url: ENV.fetch("RENDER_SERVICE_URL", "http://localhost:3001/render"),
22
+ wait_for_selector: "body",
23
+ timeout: 30,
24
+ selectors: {
25
+ content: "article, main, .content",
26
+ title: "h1, title"
27
+ }
28
+ }
29
+ end
30
+
31
+ def call
32
+ url = preferred_url
33
+ return missing_url_result unless url.present?
34
+
35
+ # Step 1: Render the page via headless service
36
+ render_result = render_page(url)
37
+ return fetch_failure(render_result) unless render_result[:success]
38
+
39
+ html = render_result[:body]
40
+
41
+ # Step 2: Extract content using CSS selectors
42
+ content = extract_content(html)
43
+ title = extract_title(html)
44
+
45
+ if content.blank?
46
+ return Result.new(
47
+ status: :partial,
48
+ html: html,
49
+ content: nil,
50
+ metadata: build_metadata(url: url, title: title, note: "No content extracted")
51
+ )
52
+ end
53
+
54
+ Result.new(
55
+ status: :success,
56
+ html: html,
57
+ content: content,
58
+ metadata: build_metadata(url: url, title: title)
59
+ )
60
+ rescue Faraday::TimeoutError => error
61
+ timeout_result(url, error)
62
+ rescue StandardError => error
63
+ error_result(url, error)
64
+ end
65
+
66
+ private
67
+
68
+ def preferred_url
69
+ item.canonical_url.presence || item.url
70
+ end
71
+
72
+ def render_page(url)
73
+ conn = http.client(timeout: settings[:timeout])
74
+ response = conn.post(settings[:render_service_url]) do |req|
75
+ req.headers["Content-Type"] = "application/json"
76
+ req.body = {
77
+ url: url,
78
+ wait_for: settings[:wait_for_selector],
79
+ timeout: (settings[:timeout].to_i * 1000)
80
+ }.to_json
81
+ end
82
+
83
+ if response.status >= 200 && response.status < 300
84
+ { success: true, body: response.body, status: response.status }
85
+ else
86
+ { success: false, status: response.status, error: "HTTP #{response.status}" }
87
+ end
88
+ rescue Faraday::Error => error
89
+ { success: false, error: error.message }
90
+ end
91
+
92
+ def extract_content(html)
93
+ return nil if html.blank?
94
+
95
+ doc = Nokogiri::HTML(html)
96
+ selector = settings.dig(:selectors, :content) || "body"
97
+
98
+ element = doc.at_css(selector)
99
+ return nil unless element
100
+
101
+ # Remove script and style tags
102
+ element.css("script, style, nav, footer, header").each(&:remove)
103
+ element.text.squeeze(" \n").strip
104
+ end
105
+
106
+ def extract_title(html)
107
+ return nil if html.blank?
108
+
109
+ doc = Nokogiri::HTML(html)
110
+ selector = settings.dig(:selectors, :title) || "title"
111
+ doc.at_css(selector)&.text&.strip
112
+ end
113
+
114
+ def build_metadata(url:, title: nil, note: nil)
115
+ meta = {
116
+ url: url,
117
+ extraction_method: "headless",
118
+ title: title
119
+ }
120
+ meta[:note] = note if note
121
+ meta.compact
122
+ end
123
+
124
+ def missing_url_result
125
+ Result.new(
126
+ status: :failed,
127
+ metadata: { error: "missing_url", message: "No URL available for scraping" }
128
+ )
129
+ end
130
+
131
+ def fetch_failure(render_result)
132
+ Result.new(
133
+ status: :failed,
134
+ metadata: {
135
+ error: "render_failed",
136
+ message: render_result[:error] || "Render service returned error",
137
+ http_status: render_result[:status]
138
+ }.compact
139
+ )
140
+ end
141
+
142
+ def timeout_result(url, error)
143
+ Result.new(
144
+ status: :failed,
145
+ metadata: {
146
+ error: "timeout",
147
+ message: error.message,
148
+ url: url
149
+ }
150
+ )
151
+ end
152
+
153
+ def error_result(url, error)
154
+ Result.new(
155
+ status: :failed,
156
+ metadata: {
157
+ error: error.class.name,
158
+ message: error.message,
159
+ url: url
160
+ }
161
+ )
162
+ end
163
+ end
164
+ end
165
+ end
166
+ ```
167
+
168
+ ## Registration
169
+
170
+ ```ruby
171
+ # config/initializers/source_monitor.rb
172
+ SourceMonitor.configure do |config|
173
+ config.scrapers.register(:headless, "MyApp::Scrapers::Headless")
174
+ end
175
+ ```
176
+
177
+ ## Per-Source Settings
178
+
179
+ Override adapter defaults via the source's `scrape_settings` JSON column:
180
+
181
+ ```ruby
182
+ source = SourceMonitor::Source.find(1)
183
+ source.update!(scrape_settings: {
184
+ render_service_url: "https://render.example.com/api/render",
185
+ wait_for_selector: ".article-content",
186
+ timeout: 60,
187
+ selectors: {
188
+ content: ".article-body",
189
+ title: ".article-title h1"
190
+ }
191
+ })
192
+ ```
193
+
194
+ ## Tests
195
+
196
+ ```ruby
197
+ require "test_helper"
198
+ require "webmock/minitest"
199
+
200
+ class HeadlessScraperTest < ActiveSupport::TestCase
201
+ setup do
202
+ @source = create_source!
203
+ @item = @source.items.create!(
204
+ title: "Test Article",
205
+ url: "https://example.com/spa-article",
206
+ external_id: "headless-test-1"
207
+ )
208
+ end
209
+
210
+ test "successfully renders and extracts content" do
211
+ stub_request(:post, "http://localhost:3001/render")
212
+ .to_return(
213
+ status: 200,
214
+ body: <<~HTML
215
+ <html>
216
+ <head><title>Test Page</title></head>
217
+ <body>
218
+ <article>
219
+ <h1>Article Title</h1>
220
+ <p>This is the article content.</p>
221
+ </article>
222
+ </body>
223
+ </html>
224
+ HTML
225
+ )
226
+
227
+ result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
228
+
229
+ assert_equal :success, result.status
230
+ assert_includes result.content, "article content"
231
+ assert_equal "headless", result.metadata[:extraction_method]
232
+ end
233
+
234
+ test "returns failed when render service is down" do
235
+ stub_request(:post, "http://localhost:3001/render")
236
+ .to_return(status: 500, body: "Internal Server Error")
237
+
238
+ result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
239
+
240
+ assert_equal :failed, result.status
241
+ assert_equal "render_failed", result.metadata[:error]
242
+ end
243
+
244
+ test "returns partial when no content found" do
245
+ stub_request(:post, "http://localhost:3001/render")
246
+ .to_return(status: 200, body: "<html><body><nav>Nav only</nav></body></html>")
247
+
248
+ result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
249
+
250
+ assert_equal :partial, result.status
251
+ assert_nil result.content
252
+ end
253
+
254
+ test "handles missing URL" do
255
+ @item.update!(url: nil)
256
+
257
+ result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
258
+
259
+ assert_equal :failed, result.status
260
+ assert_equal "missing_url", result.metadata[:error]
261
+ end
262
+
263
+ test "merges source-level settings" do
264
+ @source.update!(scrape_settings: { timeout: 60 })
265
+
266
+ stub_request(:post, "http://localhost:3001/render")
267
+ .to_return(status: 200, body: "<html><body><article>Content</article></body></html>")
268
+
269
+ result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
270
+
271
+ assert_equal :success, result.status
272
+ end
273
+ end
274
+ ```
@@ -190,3 +190,57 @@
190
190
  "title": "",
191
191
  "message": "Claude Code needs your attention"
192
192
  }
193
+ {
194
+ "timestamp": "2026-02-11T00:46:04Z",
195
+ "type": "idle_prompt",
196
+ "title": "",
197
+ "message": "Claude is waiting for your input"
198
+ }
199
+ {
200
+ "timestamp": "2026-02-11T00:59:14Z",
201
+ "type": "idle_prompt",
202
+ "title": "",
203
+ "message": "Claude is waiting for your input"
204
+ }
205
+ {
206
+ "timestamp": "2026-02-11T03:26:32Z",
207
+ "type": "idle_prompt",
208
+ "title": "",
209
+ "message": "Claude is waiting for your input"
210
+ }
211
+ {
212
+ "timestamp": "2026-02-11T03:46:35Z",
213
+ "type": "idle_prompt",
214
+ "title": "",
215
+ "message": "Claude is waiting for your input"
216
+ }
217
+ {
218
+ "timestamp": "2026-02-11T04:21:35Z",
219
+ "type": "permission_prompt",
220
+ "title": "",
221
+ "message": "Claude Code needs your attention"
222
+ }
223
+ {
224
+ "timestamp": "2026-02-11T04:28:53Z",
225
+ "type": "permission_prompt",
226
+ "title": "",
227
+ "message": "Claude Code needs your approval for the plan"
228
+ }
229
+ {
230
+ "timestamp": "2026-02-11T04:33:09Z",
231
+ "type": "permission_prompt",
232
+ "title": "",
233
+ "message": "Claude Code needs your approval for the plan"
234
+ }
235
+ {
236
+ "timestamp": "2026-02-11T04:46:56Z",
237
+ "type": "idle_prompt",
238
+ "title": "",
239
+ "message": "Claude is waiting for your input"
240
+ }
241
+ {
242
+ "timestamp": "2026-02-11T04:55:02Z",
243
+ "type": "permission_prompt",
244
+ "title": "",
245
+ "message": "Claude Code needs your approval for the plan"
246
+ }
@@ -869,3 +869,124 @@
869
869
  "other": 439
870
870
  }
871
871
  }
872
+ {
873
+ "timestamp": "2026-02-11T00:45:04Z",
874
+ "duration_ms": 0,
875
+ "cost_usd": 0,
876
+ "tokens_in": 0,
877
+ "tokens_out": 0,
878
+ "model": "unknown",
879
+ "branch": "main"
880
+ }
881
+ {
882
+ "timestamp": "2026-02-11T00:45:04Z",
883
+ "type": "cost_summary",
884
+ "costs": {
885
+ "other": 69
886
+ }
887
+ }
888
+ {
889
+ "timestamp": "2026-02-11T03:41:32Z",
890
+ "duration_ms": 0,
891
+ "cost_usd": 0,
892
+ "tokens_in": 0,
893
+ "tokens_out": 0,
894
+ "model": "unknown",
895
+ "branch": "chore/dependency-updates"
896
+ }
897
+ {
898
+ "timestamp": "2026-02-11T03:41:32Z",
899
+ "type": "cost_summary",
900
+ "costs": {
901
+ "other": 191
902
+ }
903
+ }
904
+ {
905
+ "timestamp": "2026-02-11T03:41:59Z",
906
+ "duration_ms": 0,
907
+ "cost_usd": 0,
908
+ "tokens_in": 0,
909
+ "tokens_out": 0,
910
+ "model": "unknown",
911
+ "branch": "chore/dependency-updates"
912
+ }
913
+ {
914
+ "timestamp": "2026-02-11T03:41:59Z",
915
+ "type": "cost_summary",
916
+ "costs": {
917
+ "other": 47
918
+ }
919
+ }
920
+ {
921
+ "timestamp": "2026-02-11T03:42:02Z",
922
+ "duration_ms": 0,
923
+ "cost_usd": 0,
924
+ "tokens_in": 0,
925
+ "tokens_out": 0,
926
+ "model": "unknown",
927
+ "branch": "chore/dependency-updates"
928
+ }
929
+ {
930
+ "timestamp": "2026-02-11T03:42:02Z",
931
+ "type": "cost_summary",
932
+ "costs": {
933
+ "other": 27
934
+ }
935
+ }
936
+ {
937
+ "timestamp": "2026-02-11T03:45:34Z",
938
+ "duration_ms": 0,
939
+ "cost_usd": 0,
940
+ "tokens_in": 0,
941
+ "tokens_out": 0,
942
+ "model": "unknown",
943
+ "branch": "chore/dependency-updates"
944
+ }
945
+ {
946
+ "timestamp": "2026-02-11T03:45:34Z",
947
+ "type": "cost_summary",
948
+ "costs": {
949
+ "other": 83
950
+ }
951
+ }
952
+ {
953
+ "timestamp": "2026-02-11T04:45:55Z",
954
+ "duration_ms": 0,
955
+ "cost_usd": 0,
956
+ "tokens_in": 0,
957
+ "tokens_out": 0,
958
+ "model": "unknown",
959
+ "branch": "chore/dependency-updates"
960
+ }
961
+ {
962
+ "timestamp": "2026-02-11T04:45:55Z",
963
+ "type": "cost_summary",
964
+ "costs": {
965
+ "other": 46018
966
+ }
967
+ }
968
+ {
969
+ "timestamp": "2026-02-11T04:47:49Z",
970
+ "duration_ms": 0,
971
+ "cost_usd": 0,
972
+ "tokens_in": 0,
973
+ "tokens_out": 0,
974
+ "model": "unknown",
975
+ "branch": "chore/dependency-updates"
976
+ }
977
+ {
978
+ "timestamp": "2026-02-11T04:50:36Z",
979
+ "duration_ms": 0,
980
+ "cost_usd": 0,
981
+ "tokens_in": 0,
982
+ "tokens_out": 0,
983
+ "model": "unknown",
984
+ "branch": "chore/dependency-updates"
985
+ }
986
+ {
987
+ "timestamp": "2026-02-11T04:50:36Z",
988
+ "type": "cost_summary",
989
+ "costs": {
990
+ "other": 89
991
+ }
992
+ }
data/CHANGELOG.md CHANGED
@@ -15,6 +15,15 @@ All notable changes to this project are documented below. The format follows [Ke
15
15
 
16
16
  - No unreleased changes yet.
17
17
 
18
+ ## [0.3.1] - 2026-02-10
19
+
20
+ ### Added
21
+
22
+ - 14 engine-specific Claude Code skills (`sm-*` prefix) for contributors and consumers.
23
+ - Skills installer with consumer/contributor groups via rake tasks.
24
+ - Skills installation integrated into guided `bin/source_monitor install` workflow.
25
+ - Skills packaged in gem for distribution to host apps.
26
+
18
27
  ## [0.3.0] - 2026-02-10
19
28
 
20
29
  ### Changed
data/CLAUDE.md CHANGED
@@ -177,3 +177,46 @@ These skills are available in `.claude/skills/`:
177
177
  | `performance-optimization` | Performance tuning patterns |
178
178
  | `i18n-patterns` | Internationalization patterns |
179
179
  | `active-storage-setup` | Active Storage configuration |
180
+
181
+ ## Source Monitor Skills
182
+
183
+ Engine-specific skills (`sm-*` prefix). Consumer skills install by default; contributor skills are opt-in.
184
+
185
+ ### Consumer Skills (default install)
186
+
187
+ | Skill | Purpose |
188
+ |-------|---------|
189
+ | `sm-host-setup` | Full host app setup walkthrough |
190
+ | `sm-configure` | DSL configuration across all sub-sections |
191
+ | `sm-scraper-adapter` | Custom scraper inheriting `Scrapers::Base` |
192
+ | `sm-event-handler` | Lifecycle callbacks (after_item_created, etc.) |
193
+ | `sm-model-extension` | Extend engine models from host app |
194
+ | `sm-dashboard-widget` | Dashboard queries, presenters, Turbo broadcasts |
195
+
196
+ ### Contributor Skills (opt-in)
197
+
198
+ | Skill | Purpose |
199
+ |-------|---------|
200
+ | `sm-domain-model` | Model graph, relationships, state values, scopes |
201
+ | `sm-architecture` | Module map, autoload tree, extraction patterns |
202
+ | `sm-engine-test` | Engine test helpers, VCR/WebMock, parallel caveats |
203
+ | `sm-configuration-setting` | Add settings to config sub-sections |
204
+ | `sm-pipeline-stage` | Add/modify fetch or scrape pipeline stages |
205
+ | `sm-engine-migration` | Migrations with `sourcemon_` prefix conventions |
206
+ | `sm-job` | Solid Queue jobs with shallow delegation |
207
+ | `sm-health-rule` | Health status rules, circuit breaker, auto-pause |
208
+
209
+ ### Skills Distribution
210
+
211
+ Host apps can install `sm-*` skills via rake:
212
+
213
+ ```bash
214
+ bin/rails source_monitor:skills:install # Consumer skills (default)
215
+ bin/rails source_monitor:skills:contributor # Contributor skills
216
+ bin/rails source_monitor:skills:all # All skills
217
+ bin/rails source_monitor:skills:remove # Remove all sm-* skills
218
+ ```
219
+
220
+ ## Maintenance Rules
221
+
222
+ - **Skills & docs alignment**: Whenever engine code changes (models, configuration, pipeline, jobs, migrations, scrapers, events, health rules, or dashboard), the corresponding `sm-*` skill and its `reference/` files MUST be updated in the same PR to ensure skills always reflect current engine behavior.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- source_monitor (0.3.0)
4
+ source_monitor (0.3.1)
5
5
  cssbundling-rails (~> 1.4)
6
6
  faraday (~> 2.9)
7
7
  faraday-follow_redirects (~> 0.4)
@@ -99,7 +99,7 @@ GEM
99
99
  ast (2.4.3)
100
100
  base64 (0.3.0)
101
101
  bigdecimal (4.0.1)
102
- brakeman (7.1.1)
102
+ brakeman (8.0.2)
103
103
  racc
104
104
  builder (3.3.0)
105
105
  capybara (3.40.0)
@@ -128,17 +128,17 @@ GEM
128
128
  erubi (1.13.1)
129
129
  et-orbi (1.4.0)
130
130
  tzinfo
131
- faraday (2.14.0)
131
+ faraday (2.14.1)
132
132
  faraday-net_http (>= 2.0, < 3.5)
133
133
  json
134
134
  logger
135
135
  faraday-follow_redirects (0.5.0)
136
136
  faraday (>= 1, < 3)
137
- faraday-gzip (3.0.4)
137
+ faraday-gzip (3.1.0)
138
138
  faraday (>= 2.0, < 3)
139
139
  zlib (~> 3.0)
140
- faraday-net_http (3.4.1)
141
- net-http (>= 0.5.0)
140
+ faraday-net_http (3.4.2)
141
+ net-http (~> 0.5)
142
142
  faraday-retry (2.4.0)
143
143
  faraday (~> 2.0)
144
144
  feedjira (4.0.1)
@@ -182,8 +182,8 @@ GEM
182
182
  minitest (6.0.1)
183
183
  prism (~> 1.5)
184
184
  minitest-mock (5.27.0)
185
- net-http (0.6.0)
186
- uri
185
+ net-http (0.9.1)
186
+ uri (>= 0.11.1)
187
187
  net-imap (0.6.2)
188
188
  date
189
189
  net-protocol
@@ -222,13 +222,13 @@ GEM
222
222
  parser (3.3.10.1)
223
223
  ast (~> 2.4.1)
224
224
  racc
225
- pg (1.6.2)
226
- pg (1.6.2-aarch64-linux)
227
- pg (1.6.2-aarch64-linux-musl)
228
- pg (1.6.2-arm64-darwin)
229
- pg (1.6.2-x86_64-darwin)
230
- pg (1.6.2-x86_64-linux)
231
- pg (1.6.2-x86_64-linux-musl)
225
+ pg (1.6.3)
226
+ pg (1.6.3-aarch64-linux)
227
+ pg (1.6.3-aarch64-linux-musl)
228
+ pg (1.6.3-arm64-darwin)
229
+ pg (1.6.3-x86_64-darwin)
230
+ pg (1.6.3-x86_64-linux)
231
+ pg (1.6.3-x86_64-linux-musl)
232
232
  pp (0.6.3)
233
233
  prettyprint
234
234
  prettyprint (0.2.0)
@@ -326,13 +326,13 @@ GEM
326
326
  rubocop-performance (>= 1.24)
327
327
  rubocop-rails (>= 2.30)
328
328
  ruby-progressbar (1.13.0)
329
- ruby-readability (0.7.2)
329
+ ruby-readability (0.7.3)
330
330
  guess_html_encoding (>= 0.0.4)
331
331
  nokogiri (>= 1.6.0)
332
332
  rubyzip (3.2.2)
333
333
  sax-machine (1.3.2)
334
334
  securerandom (0.4.1)
335
- selenium-webdriver (4.38.0)
335
+ selenium-webdriver (4.40.0)
336
336
  base64 (~> 0.2)
337
337
  logger (~> 1.4)
338
338
  rexml (~> 3.2, >= 3.2.5)
@@ -358,7 +358,7 @@ GEM
358
358
  thor (>= 1.3.1)
359
359
  stackprof (0.2.27)
360
360
  stringio (3.2.0)
361
- test-prof (1.4.4)
361
+ test-prof (1.5.2)
362
362
  thor (1.5.0)
363
363
  timeout (0.6.0)
364
364
  tsort (0.2.0)
@@ -372,8 +372,7 @@ GEM
372
372
  unicode-emoji (4.2.0)
373
373
  uri (1.1.1)
374
374
  useragent (0.16.11)
375
- vcr (6.3.1)
376
- base64
375
+ vcr (6.4.0)
377
376
  webmock (3.26.1)
378
377
  addressable (>= 2.8.0)
379
378
  crack (>= 0.3.2)
@@ -386,7 +385,7 @@ GEM
386
385
  xpath (3.2.0)
387
386
  nokogiri (~> 1.8)
388
387
  zeitwerk (2.7.4)
389
- zlib (3.2.1)
388
+ zlib (3.2.2)
390
389
 
391
390
  PLATFORMS
392
391
  aarch64-linux-gnu
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
+ require_relative "skills_installer"
4
5
 
5
6
  module SourceMonitor
6
7
  module Setup
@@ -35,7 +36,8 @@ module SourceMonitor
35
36
  migration_installer: MigrationInstaller.new,
36
37
  initializer_patcher: InitializerPatcher.new,
37
38
  devise_detector: method(:default_devise_detector),
38
- verifier: Verification::Runner.new
39
+ verifier: Verification::Runner.new,
40
+ skills_installer: SkillsInstaller.new
39
41
  )
40
42
  @dependency_checker = dependency_checker
41
43
  @prompter = prompter
@@ -47,6 +49,7 @@ module SourceMonitor
47
49
  @initializer_patcher = initializer_patcher
48
50
  @devise_detector = devise_detector
49
51
  @verifier = verifier
52
+ @skills_installer = skills_installer
50
53
  end
51
54
 
52
55
  def run
@@ -67,6 +70,13 @@ module SourceMonitor
67
70
  end
68
71
 
69
72
  verifier.call
73
+
74
+ if prompter.yes?("Install Claude Code skills for using SourceMonitor?", default: true)
75
+ skills_installer.install(target_dir: skills_target_dir, group: :consumer)
76
+ if prompter.yes?("Also install contributor skills for engine development?", default: false)
77
+ skills_installer.install(target_dir: skills_target_dir, group: :contributor)
78
+ end
79
+ end
70
80
  end
71
81
 
72
82
  private
@@ -80,7 +90,12 @@ module SourceMonitor
80
90
  :migration_installer,
81
91
  :initializer_patcher,
82
92
  :devise_detector,
83
- :verifier
93
+ :verifier,
94
+ :skills_installer
95
+
96
+ def skills_target_dir
97
+ File.join(Dir.pwd, ".claude", "skills")
98
+ end
84
99
 
85
100
  def devise_available?
86
101
  !!devise_detector.call
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SourceMonitor
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end