rails-ai-context 5.36.3 → 5.36.4
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/CHANGELOG.md +25 -0
- data/SECURITY.md +1 -1
- data/docs/COMPATIBILITY.md +6 -0
- data/docs/CONFIGURATION.md +1 -0
- data/docs/SECURITY.md +1 -1
- data/docs/STANDALONE.md +20 -0
- data/exe/rails-ai-context +28 -1
- data/lib/rails_ai_context/ast_cache.rb +5 -3
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +1 -1
- data/lib/rails_ai_context/introspectors/listeners/queue_assignment_listener.rb +4 -2
- data/lib/rails_ai_context/introspectors/node_source.rb +9 -4
- data/lib/rails_ai_context/server.rb +9 -5
- data/lib/rails_ai_context/tools/onboard.rb +2 -1
- data/lib/rails_ai_context/update_check.rb +149 -0
- data/lib/rails_ai_context/version.rb +1 -1
- 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: d0dd31bd07cbfa7f5cb1e1232627cda1c7ea16f035f864d8db86e1f544bb3893
|
|
4
|
+
data.tar.gz: 92512c007d7fc751a59bf72b1f651fb2a6e58117bc47697e109c145cbd65b6cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d751847ce7745680c03f69a54077e1f8a1836c37e788d78ad5072792af68be4f7e531b98e5bc3b62159e8b297259d8ebc2649ee22c41b6150edc90e9faf0c69
|
|
7
|
+
data.tar.gz: a64566fdec0abb014195d48dcf81d2463a24b9c72addaec1254dcac0778e6f4190805baddfdf073ec2ca5b847fe4ebc4ba643de759b46fc5a16e13b586e80dab
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [5.36.4] - 2026-10-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **The standalone binary says when a newer release is out.** A copy installed
|
|
13
|
+
with `gem install` stayed at its version until someone thought to update it.
|
|
14
|
+
It now asks rubygems.org for the latest version at most once a day, in a
|
|
15
|
+
detached child process, and keeps the answer under `$XDG_STATE_HOME`
|
|
16
|
+
(`~/.local/state/rails-ai-context/latest`). A command run in a terminal prints
|
|
17
|
+
one line on stderr naming the Ruby to update under and the two commands to
|
|
18
|
+
run. The MCP server adds the line to its instructions and to the end of
|
|
19
|
+
`rails_onboard` for the agent to pass on, or asks for a restart when the new
|
|
20
|
+
version is already installed. `RAILS_AI_CONTEXT_NO_UPDATE_CHECK=1` turns it
|
|
21
|
+
off. It is off in CI, on a prerelease, for an older version run on purpose
|
|
22
|
+
(`_5.30.0_`) and in a copy the app's bundle runs. Nothing is installed for you.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **The code reads prism 2 the way it reads prism 1.** prism's main branch
|
|
27
|
+
(2.0.0.rc1) parses with the running Ruby's grammar when it is given none,
|
|
28
|
+
replaced `MissingNode` with `ErrorRecoveryNode`, and leaves an unterminated
|
|
29
|
+
heredoc without a closing location. The gem now names the newest grammar
|
|
30
|
+
when it parses and lexes, reads either recovery node, and ends an
|
|
31
|
+
unterminated heredoc at its body. The gemspec still requires prism below 2.0.
|
|
32
|
+
|
|
8
33
|
## [5.36.3] - 2026-10-15
|
|
9
34
|
|
|
10
35
|
### Fixed
|
data/SECURITY.md
CHANGED
|
@@ -32,4 +32,4 @@ If you discover a security vulnerability in rails-ai-context, please report it r
|
|
|
32
32
|
- **Migration input validation** - `rails_migration_advisor` validates table and column names as safe identifiers before generating migration code.
|
|
33
33
|
- **Cache invalidation coverage** - Fingerprinter watches `app/components`, `package.json`, and `tsconfig.json` alongside models/controllers/views to prevent stale tool responses.
|
|
34
34
|
- **Fetch size limits** - `rails_search_docs` caps fetched documentation content at 2MB to prevent memory exhaustion.
|
|
35
|
-
- The gem makes outbound HTTPS requests
|
|
35
|
+
- The gem makes outbound HTTPS requests in two places. `rails_search_docs` fetches Rails documentation from GitHub raw content when called with `fetch: true`. The standalone binary (installed with `gem install`, not through the app's Gemfile) asks `https://rubygems.org/api/v1/versions/rails-ai-context/latest.json` for the latest version at most once a day, in a child process that sends nothing about the app, and writes the answer to `rails-ai-context/latest` under `$XDG_STATE_HOME` (`~/.local/state`). `RAILS_AI_CONTEXT_NO_UPDATE_CHECK=1` turns it off; it is off in CI and in a copy the app's bundle runs. All other tools are offline.
|
data/docs/COMPATIBILITY.md
CHANGED
|
@@ -340,6 +340,12 @@ Proof sources:
|
|
|
340
340
|
with Rails 7.0 and 7.2, Ruby 3.3 with Rails 7.1, Ruby 3.4 with Rails 8.0 and
|
|
341
341
|
8.1, and Ruby 4.0 with Rails 8.1; and the unit suite and the E2E harness on
|
|
342
342
|
Linux, macOS and Windows.
|
|
343
|
+
In the v5.36.4 release QA: the update check run as the installed binary on
|
|
344
|
+
Ruby 3.1, 3.3, 3.4 and 4.0 against rubygems.org, through a terminal and over
|
|
345
|
+
MCP stdio on a fixture app, a large private app booted and another in the
|
|
346
|
+
static tier, with the app's pinned json and net-http still loaded; the
|
|
347
|
+
suite on prism 1.4.0 and on prism main under Ruby 3.2 and 3.4; and the unit
|
|
348
|
+
suite and the E2E harness on Linux, macOS and Windows.
|
|
343
349
|
2. Non-crash coverage for every built-in tool including `get_view` in
|
|
344
350
|
`spec/e2e/in_gemfile_install_spec.rb`'s full-tool sweep; output correctness
|
|
345
351
|
(ivar cross-check, render-form detection, partial interfaces) verified
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -266,6 +266,7 @@ server entry's `env`.
|
|
|
266
266
|
| Variable | Read by | Effect |
|
|
267
267
|
|:---------|:--------|:-------|
|
|
268
268
|
| `RAILS_AI_CONTEXT_BOOT_TIMEOUT` | Every command that boots the app, and the `ai:serve`, `ai:serve_http` and `ai:tool` rake tasks | Seconds a boot may take before it counts as failed; a command that can then answers from the static tier. Default 20 for `serve` over stdio and `rails ai:serve`, whose client waits about 30 seconds for its first answer, and 60 for everything else. A value that is not a whole number warns and keeps the default |
|
|
269
|
+
| `RAILS_AI_CONTEXT_NO_UPDATE_CHECK` | The standalone binary | Any non-empty value turns off the daily check for a newer release on rubygems.org. The check also stays off in CI, on a prerelease and in a copy the app's bundle runs |
|
|
269
270
|
| `RAILS_AI_CONTEXT_SERVER_NAME` | The MCP server | The name it announces for one process, in place of the default `server_name`; a `server_name` the app sets wins. A workspace's MCP entries set it |
|
|
270
271
|
| `DEBUG` | Every command and the MCP server | Any value prints the backtrace of a failed boot, a failed command and an exception that escapes a tool, and names the errors a fallback swallows |
|
|
271
272
|
| `RAILS_ENV`, `RACK_ENV` | Every command | The Rails environment to boot, read the way Rails reads it; `--environment` sets it for one command. Default `development` |
|
data/docs/SECURITY.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
1. **Read-only by design** - All 35 tools are annotated as non-destructive in the MCP protocol
|
|
19
19
|
2. **Defense in depth** - Multiple security layers, not single points of failure
|
|
20
20
|
3. **Sensitive data blocking** - Configurable patterns prevent access to secrets
|
|
21
|
-
4. **Offline
|
|
21
|
+
4. **Offline except where named** - No network calls except optional `rails_search_docs` with `fetch: true`, and the standalone binary's daily version check against rubygems.org (`RAILS_AI_CONTEXT_NO_UPDATE_CHECK=1` turns it off)
|
|
22
22
|
5. **Graceful degradation** - Missing optional dependencies don't expose errors or state
|
|
23
23
|
|
|
24
24
|
---
|
data/docs/STANDALONE.md
CHANGED
|
@@ -140,6 +140,26 @@ Codex CLI is special - it `env_clear()`s the process before spawning MCP servers
|
|
|
140
140
|
|
|
141
141
|
If you switch Ruby versions, re-run `rails-ai-context init` to update the snapshot.
|
|
142
142
|
|
|
143
|
+
## Updating
|
|
144
|
+
|
|
145
|
+
A gem installed with `gem install` stays at its version until you update it:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
gem update rails-ai-context
|
|
149
|
+
rails-ai-context init --defaults # in each app: brings its MCP configs and context files up to the new version
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Each Ruby has its own copy, so update under the Ruby your AI client starts the
|
|
153
|
+
server with. Restart the AI client afterwards so its MCP server runs the new copy.
|
|
154
|
+
|
|
155
|
+
The binary checks rubygems.org for a newer release at most once a day, in the
|
|
156
|
+
background. When there is one, a command run in a terminal says so on stderr,
|
|
157
|
+
and the MCP server tells the agent to pass it on, in its instructions and at the
|
|
158
|
+
end of `rails_onboard`. A server reads the answer the check left the last time,
|
|
159
|
+
so the first check shows up from the next start. `RAILS_AI_CONTEXT_NO_UPDATE_CHECK=1` turns the check off.
|
|
160
|
+
It stays off in CI and in a copy the app's bundle runs, where the lockfile picks
|
|
161
|
+
the version.
|
|
162
|
+
|
|
143
163
|
## Switching between standalone and in-Gemfile
|
|
144
164
|
|
|
145
165
|
You can switch freely:
|
data/exe/rails-ai-context
CHANGED
|
@@ -201,7 +201,10 @@ class RailsAiContextCLI < Thor
|
|
|
201
201
|
# One answer for any command's failure; Thor's refusals and SystemExit pass through.
|
|
202
202
|
no_commands do
|
|
203
203
|
def invoke_command(command, *args)
|
|
204
|
-
|
|
204
|
+
start_update_check
|
|
205
|
+
result = super
|
|
206
|
+
check_for_update
|
|
207
|
+
result
|
|
205
208
|
rescue Thor::Error
|
|
206
209
|
raise
|
|
207
210
|
rescue StandardError, ScriptError => e
|
|
@@ -319,6 +322,7 @@ class RailsAiContextCLI < Thor
|
|
|
319
322
|
# No Thor default: it would overwrite the config file's http_port.
|
|
320
323
|
::RailsAiContext.configuration.http_port = options[:port] if options[:port]
|
|
321
324
|
|
|
325
|
+
start_update_check&.refresh(env: STARTING_ENV)
|
|
322
326
|
::RailsAiContext.start_mcp_server(transport: transport)
|
|
323
327
|
end
|
|
324
328
|
|
|
@@ -1326,6 +1330,29 @@ class RailsAiContextCLI < Thor
|
|
|
1326
1330
|
nil
|
|
1327
1331
|
end
|
|
1328
1332
|
|
|
1333
|
+
# A notice on a terminal only: stdout may be piped, and an agent reads rails_onboard's copy instead.
|
|
1334
|
+
def check_for_update
|
|
1335
|
+
check = start_update_check or return
|
|
1336
|
+
check.refresh(env: STARTING_ENV)
|
|
1337
|
+
note = check.notice unless check.told?
|
|
1338
|
+
$stderr.puts note if note && $stderr.tty?
|
|
1339
|
+
rescue StandardError, ScriptError
|
|
1340
|
+
nil
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
# Only this copy run outside a bundle checks; under bundle exec the app's lockfile picks the version.
|
|
1344
|
+
# Started before the command, while Gem.path is still the one an app's boot replaces.
|
|
1345
|
+
def start_update_check
|
|
1346
|
+
return if STARTING_ENV["BUNDLE_BIN_PATH"]
|
|
1347
|
+
|
|
1348
|
+
@start_update_check ||= begin
|
|
1349
|
+
require_relative "../lib/rails_ai_context/update_check"
|
|
1350
|
+
::RailsAiContext::UpdateCheck.tap(&:enable!)
|
|
1351
|
+
end
|
|
1352
|
+
rescue StandardError, ScriptError
|
|
1353
|
+
nil
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1329
1356
|
# Stdlib only, so it is safe to load before the app boots.
|
|
1330
1357
|
def entry_boot
|
|
1331
1358
|
@entry_boot ||= begin
|
|
@@ -80,14 +80,14 @@ module RailsAiContext
|
|
|
80
80
|
# A one-shot parse that stores nothing: for a scan reading thousands of
|
|
81
81
|
# files once, whose entries would only evict what other readers reuse.
|
|
82
82
|
def self.parse_uncached(source)
|
|
83
|
-
Prism.parse(source)
|
|
83
|
+
Prism.parse(source, version: NEWEST_GRAMMAR)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# Whether a file parses under `ruby:`'s grammar, with no tree built or
|
|
87
87
|
# kept: the check before a loader requires each file it walks.
|
|
88
88
|
def self.parse_success?(path, ruby: nil)
|
|
89
89
|
version = prism_version(ruby)
|
|
90
|
-
|
|
90
|
+
Prism.parse_file_success?(path, version: version || NEWEST_GRAMMAR)
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# Parse a Ruby source string, cached by content digest, so every extractor
|
|
@@ -112,6 +112,8 @@ module RailsAiContext
|
|
|
112
112
|
private_class_method :content_key
|
|
113
113
|
|
|
114
114
|
OLDEST_GRAMMAR = "3.3"
|
|
115
|
+
# Named, because prism 2 parses with the running Ruby's grammar when given none.
|
|
116
|
+
NEWEST_GRAMMAR = "latest"
|
|
115
117
|
GRAMMARS = Concurrent::Map.new # major.minor => whether the loaded prism parses it
|
|
116
118
|
|
|
117
119
|
# The grammar prism parses `ruby` with: its own major.minor, the oldest prism
|
|
@@ -134,7 +136,7 @@ module RailsAiContext
|
|
|
134
136
|
private_class_method :supported?
|
|
135
137
|
|
|
136
138
|
def self.prism_parse(source, version)
|
|
137
|
-
|
|
139
|
+
Prism.parse(source, version: version || NEWEST_GRAMMAR)
|
|
138
140
|
end
|
|
139
141
|
private_class_method :prism_parse
|
|
140
142
|
|
|
@@ -9,6 +9,9 @@ module RailsAiContext
|
|
|
9
9
|
# and Que's `self.queue = "name"`, or a `def self.queue` returning one, which
|
|
10
10
|
# Resque asks for when @queue is unset. A literal is kept as its value, anything else as source.
|
|
11
11
|
class QueueAssignmentListener < BaseListener
|
|
12
|
+
# What a half-written `@queue =` parses its value as: MissingNode before prism 2, ErrorRecoveryNode from it.
|
|
13
|
+
RECOVERY_NODES = %i[MissingNode ErrorRecoveryNode].filter_map { |name| Prism.const_get(name) if Prism.const_defined?(name, false) }.freeze
|
|
14
|
+
|
|
12
15
|
def initialize
|
|
13
16
|
super
|
|
14
17
|
@def_depth = 0
|
|
@@ -40,9 +43,8 @@ module RailsAiContext
|
|
|
40
43
|
|
|
41
44
|
private
|
|
42
45
|
|
|
43
|
-
# A half-written `@queue =` parses with a MissingNode value.
|
|
44
46
|
def record(node, value, form)
|
|
45
|
-
return if value.is_a?(
|
|
47
|
+
return if RECOVERY_NODES.any? { |type| value.is_a?(type) }
|
|
46
48
|
|
|
47
49
|
literal = value.is_a?(Prism::SymbolNode) || value.is_a?(Prism::StringNode)
|
|
48
50
|
@results << { form: form, queue: literal ? value.unescaped : nil, source: value.slice,
|
|
@@ -18,12 +18,12 @@ module RailsAiContext
|
|
|
18
18
|
# @return [String] the node's slice, followed by the body of every
|
|
19
19
|
# heredoc it opens whose body lies past its end
|
|
20
20
|
def text(node)
|
|
21
|
-
trailing = heredocs(node).select { |doc| doc.
|
|
21
|
+
trailing = heredocs(node).select { |doc| body_end(doc).end_offset > node.location.end_offset }
|
|
22
22
|
return node.slice if trailing.empty?
|
|
23
23
|
|
|
24
24
|
first = trailing.min_by { |doc| body_start(doc).start_offset }
|
|
25
|
-
last = trailing.max_by { |doc| doc.
|
|
26
|
-
"#{node.slice}\n#{body_start(first).join(last
|
|
25
|
+
last = trailing.max_by { |doc| body_end(doc).end_offset }
|
|
26
|
+
"#{node.slice}\n#{body_start(first).join(body_end(last)).slice}"
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def heredocs(node)
|
|
@@ -40,9 +40,14 @@ module RailsAiContext
|
|
|
40
40
|
def body_start(doc)
|
|
41
41
|
if doc.respond_to?(:content_loc) then doc.content_loc
|
|
42
42
|
elsif doc.parts.any? then doc.parts.first.location
|
|
43
|
-
else doc
|
|
43
|
+
else body_end(doc)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
|
+
|
|
47
|
+
# The terminator; an unterminated heredoc has none from prism 2 on, and ends with its body.
|
|
48
|
+
def body_end(doc)
|
|
49
|
+
doc.closing_loc || (doc.content_loc if doc.respond_to?(:content_loc)) || doc.parts.last&.location || doc.location
|
|
50
|
+
end
|
|
46
51
|
end
|
|
47
52
|
end
|
|
48
53
|
end
|
|
@@ -273,11 +273,15 @@ module RailsAiContext
|
|
|
273
273
|
# code changed (BaseTool.stale_code_note).
|
|
274
274
|
def self.instructions
|
|
275
275
|
intro = "Ground truth engine for Rails apps. Live Prism AST introspection."
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
"
|
|
280
|
-
|
|
276
|
+
text = if CodeReloader.reloadable? || RailsAiContext.static_tier?
|
|
277
|
+
"#{intro} Answers follow edits to the app's files."
|
|
278
|
+
else
|
|
279
|
+
"#{intro} Answers follow edits to the app's files, except what reflection reads, such as associations: " \
|
|
280
|
+
"RAILS_ENV=#{RailsAiContext.environment_name} does not reload code, so that stays as of boot, and every " \
|
|
281
|
+
"answer says when app code changed since."
|
|
282
|
+
end
|
|
283
|
+
note = UpdateCheck.agent_note
|
|
284
|
+
note ? "#{text} #{note}" : text
|
|
281
285
|
end
|
|
282
286
|
|
|
283
287
|
# Anything that still escapes a tool (schema validation bugs, SDK-level
|
|
@@ -46,7 +46,8 @@ module RailsAiContext
|
|
|
46
46
|
else
|
|
47
47
|
compose_standard(ctx)
|
|
48
48
|
end
|
|
49
|
-
|
|
49
|
+
update = RailsAiContext::UpdateCheck.agent_note
|
|
50
|
+
text_response(body, suffix: [ introspection_warnings_note(ctx), ("\n\n---\n#{update}" if update) ].compact.join)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
class << self
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
|
|
6
|
+
module RailsAiContext
|
|
7
|
+
# Tells the copy `gem install` put on PATH that rubygems.org has a newer
|
|
8
|
+
# release. Only the gem's own binary, run outside a bundle, turns it on
|
|
9
|
+
# (enable!), so a copy an app's bundle loads never checks: its lockfile
|
|
10
|
+
# decides the version.
|
|
11
|
+
#
|
|
12
|
+
# Core Ruby only. The binary loads this before the app boots, and requiring
|
|
13
|
+
# net/http or json here would activate the newest installed versions over
|
|
14
|
+
# the ones the app's lock pins. The fetch runs in a detached child instead,
|
|
15
|
+
# at most once a day, and the version it writes is what the next check reads.
|
|
16
|
+
module UpdateCheck
|
|
17
|
+
OPT_OUT_ENV = "RAILS_AI_CONTEXT_NO_UPDATE_CHECK"
|
|
18
|
+
URL = "https://rubygems.org/api/v1/versions/rails-ai-context/latest.json"
|
|
19
|
+
INTERVAL = 24 * 60 * 60
|
|
20
|
+
|
|
21
|
+
FETCH = <<~RUBY
|
|
22
|
+
require "net/http"
|
|
23
|
+
path, url = ARGV
|
|
24
|
+
uri = URI(url)
|
|
25
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 5, read_timeout: 5) do |http|
|
|
26
|
+
http.get(uri.request_uri)
|
|
27
|
+
end
|
|
28
|
+
version = response.body[/"version"\\s*:\\s*"([^"]+)"/, 1] if response.is_a?(Net::HTTPSuccess)
|
|
29
|
+
exit unless version
|
|
30
|
+
tmp = "\#{path}.\#{Process.pid}"
|
|
31
|
+
begin
|
|
32
|
+
File.write(tmp, version)
|
|
33
|
+
File.rename(tmp, path)
|
|
34
|
+
ensure
|
|
35
|
+
File.delete(tmp) if File.exist?(tmp)
|
|
36
|
+
end
|
|
37
|
+
RUBY
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# Called before an app's boot, which swaps Gem.path for the bundle's.
|
|
42
|
+
def enable!
|
|
43
|
+
@enabled = true
|
|
44
|
+
@gem_dirs = Gem.path
|
|
45
|
+
newest_at_start
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The notice for an agent to pass on. Giving it marks it told, so the CLI does not print it again.
|
|
49
|
+
def agent_note
|
|
50
|
+
note = notice or return
|
|
51
|
+
@told = true
|
|
52
|
+
"Tell the user: #{note}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def told?
|
|
56
|
+
@told == true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def disabled?
|
|
60
|
+
!@enabled || !ENV[OPT_OUT_ENV].to_s.empty? || ci? || Gem::Version.new(VERSION).prerelease?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Starts the fetch when the last one is a day old (or in the future, after a
|
|
64
|
+
# clock change). The file's time is set first, so a failed fetch waits a day
|
|
65
|
+
# too. Its own process group keeps a Ctrl-C at the terminal from killing it.
|
|
66
|
+
# @param env [Hash] the environment the binary started in, before an app's boot changed it
|
|
67
|
+
def refresh(env:)
|
|
68
|
+
return if disabled?
|
|
69
|
+
|
|
70
|
+
path = state_file
|
|
71
|
+
age = Time.now - File.mtime(path) if File.exist?(path)
|
|
72
|
+
return if age&.between?(0, INTERVAL)
|
|
73
|
+
|
|
74
|
+
make_dirs(File.dirname(path))
|
|
75
|
+
File.open(path, "a") { nil }
|
|
76
|
+
File.utime(nil, nil, path)
|
|
77
|
+
group = Gem.win_platform? ? { new_pgroup: true } : { pgroup: true }
|
|
78
|
+
pid = Process.spawn(env, RbConfig.ruby, "-e", FETCH, path, URL,
|
|
79
|
+
unsetenv_others: true, in: File::NULL, out: File::NULL, err: File::NULL, **group)
|
|
80
|
+
Process.detach(pid)
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# One sentence for the user, or nil when this copy is current.
|
|
86
|
+
def notice
|
|
87
|
+
return if disabled?
|
|
88
|
+
|
|
89
|
+
latest = latest_release or return
|
|
90
|
+
running = Gem::Version.new(VERSION)
|
|
91
|
+
return unless latest > running
|
|
92
|
+
# A newer copy installed when this process started means an older one was run on purpose (`_5.30.0_`).
|
|
93
|
+
return if newest_at_start && newest_at_start > running
|
|
94
|
+
|
|
95
|
+
installed = newest_installed
|
|
96
|
+
if installed && installed >= latest
|
|
97
|
+
"rails-ai-context #{installed} is installed, but this process still runs #{VERSION}. " \
|
|
98
|
+
"Restart the MCP server (or the AI client) to load it."
|
|
99
|
+
else
|
|
100
|
+
"rails-ai-context #{latest} is out; this is #{VERSION} on Ruby #{RUBY_VERSION} (#{RbConfig.ruby}). " \
|
|
101
|
+
"Under that Ruby run `gem update rails-ai-context`, then `rails-ai-context init --defaults` in each app. " \
|
|
102
|
+
"#{OPT_OUT_ENV}=1 turns this check off."
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def latest_release
|
|
107
|
+
text = File.read(state_file, 64).to_s.strip
|
|
108
|
+
return if text.empty? || !Gem::Version.correct?(text)
|
|
109
|
+
|
|
110
|
+
version = Gem::Version.new(text)
|
|
111
|
+
version unless version.prerelease?
|
|
112
|
+
rescue StandardError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def state_file
|
|
117
|
+
home = Gem.respond_to?(:state_home) ? Gem.state_home : (ENV["XDG_STATE_HOME"] || File.join(Gem.user_home, ".local", "state"))
|
|
118
|
+
File.join(home, "rails-ai-context", "latest")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The newest copy installed for this Ruby, in any directory of the gem path.
|
|
122
|
+
def newest_installed
|
|
123
|
+
Array(@gem_dirs).flat_map { |dir| Dir[File.join(dir, "specifications", "rails-ai-context-*.gemspec")] }.filter_map do |spec|
|
|
124
|
+
number = File.basename(spec, ".gemspec").delete_prefix("rails-ai-context-")
|
|
125
|
+
Gem::Version.new(number) if Gem::Version.correct?(number)
|
|
126
|
+
end.max
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def newest_at_start
|
|
130
|
+
return @newest_at_start if defined?(@newest_at_start)
|
|
131
|
+
|
|
132
|
+
@newest_at_start = newest_installed
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def ci?
|
|
136
|
+
defined?(Gem::CIDetector) ? Gem::CIDetector.ci? : !ENV["CI"].to_s.empty?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Dir.mkdir one level at a time: fileutils is a gem an app may pin.
|
|
140
|
+
def make_dirs(dir)
|
|
141
|
+
return if Dir.exist?(dir)
|
|
142
|
+
|
|
143
|
+
make_dirs(File.dirname(dir))
|
|
144
|
+
Dir.mkdir(dir)
|
|
145
|
+
rescue Errno::EEXIST
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-ai-context
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.36.
|
|
4
|
+
version: 5.36.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -560,6 +560,7 @@ files:
|
|
|
560
560
|
- lib/rails_ai_context/tools/session_context.rb
|
|
561
561
|
- lib/rails_ai_context/tools/validate.rb
|
|
562
562
|
- lib/rails_ai_context/tools/validate_semantics.rb
|
|
563
|
+
- lib/rails_ai_context/update_check.rb
|
|
563
564
|
- lib/rails_ai_context/version.rb
|
|
564
565
|
- lib/rails_ai_context/vfs.rb
|
|
565
566
|
- lib/rails_ai_context/view_file.rb
|