gemchat 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.
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require "pathname"
5
+
6
+ module Gemchat
7
+ # The sole source of file:line and signature metadata, per §11 of the plan.
8
+ # ri tells us a class and a method; it does not tell us which file the method is
9
+ # defined in, or on which line. That is the information an agent cannot recover
10
+ # from the result itself, and it is what a citation needs.
11
+ #
12
+ # Ported from the app's `RubySourceDefinitionService`, minus Rails: no
13
+ # `Rails.logger`, no ActiveSupport's `blank?`/`presence`, no `filter_map` on
14
+ # an Array that is sometimes a Hash. The visitor logic is unchanged, including
15
+ # its known imprecision -- a method defined inside `extend SomeModule` is
16
+ # reported under the lexical owner rather than the mixin. §4.4 argues that
17
+ # reporting the lexical owner is arguably *more* precise, since it says the
18
+ # method comes from a mixin.
19
+ class Symbols
20
+ # The same caps the app uses. A generated or vendored tree can contain tens of
21
+ # thousands of .rb files, and this pass runs on every indexed gem.
22
+ MAX_FILES = 2000
23
+ MAX_FILE_BYTES = 1_000_000
24
+
25
+ Definition = Struct.new(
26
+ :class_name, :method_name, :method_type, :signature, :source_path, :source_line
27
+ ) do
28
+ # Looked up against an ri chunk, which knows class/method/method_type but
29
+ # not a file. A missing owner on either side means the chunk is a class-level
30
+ # or top-level definition and there is nothing to match.
31
+ def key
32
+ [class_name.to_s, method_name.to_s, method_type.to_s]
33
+ end
34
+ end
35
+
36
+ # Definitions for every .rb under `dir`, keyed by Definition#key.
37
+ def self.for(dir)
38
+ new(dir).index
39
+ end
40
+
41
+ def initialize(dir)
42
+ @dir = dir.to_s
43
+ end
44
+
45
+ def index
46
+ return {} unless File.directory?(@dir)
47
+
48
+ files = Dir.glob(File.join(@dir, "**", "*.rb")).sort.first(MAX_FILES)
49
+ defs = files.flat_map { |path| parse_file(path) }
50
+
51
+ # First definition wins. A gem that reopens a class in several files has
52
+ # several definitions for one key, and the first in sorted order is the
53
+ # most predictable choice. Duplicates are counted so the caller can report
54
+ # an ambiguous match rather than pretending there was only one.
55
+ defs.group_by(&:key).transform_values(&:first).tap { |out| @collisions = defs.size - out.size }
56
+ end
57
+
58
+ def collisions
59
+ @collisions.to_i
60
+ end
61
+
62
+ private
63
+
64
+ def parse_file(path)
65
+ return [] if File.size(path) > MAX_FILE_BYTES
66
+
67
+ source = File.read(path, encoding: Encoding::UTF_8)
68
+ result = Prism.parse(source)
69
+ relative = Pathname.new(path).relative_path_from(Pathname.new(@dir)).to_s
70
+ Visitor.new(relative).tap { |v| v.visit(result.value) }.definitions
71
+ rescue SystemCallError, ArgumentError, Prism::ParseError => e
72
+ # One unparseable or unreadable file must not cost the whole gem its source
73
+ # locations. A gem with a syntax error in one file still has thousands of
74
+ # parseable ones.
75
+ warn " symbols: skipping #{File.basename(path)} (#{e.class})" if ENV["GEMCHAT_DEBUG"]
76
+ []
77
+ end
78
+
79
+ # Tracks class/module nesting and `class << self` scope.
80
+ class Visitor < Prism::Visitor
81
+ attr_reader :definitions
82
+
83
+ def initialize(source_path)
84
+ @source_path = source_path
85
+ @definitions = []
86
+ @namespace = []
87
+ @singleton_depth = 0
88
+ end
89
+
90
+ def visit_module_node(node)
91
+ with_namespace(constant_name(node.constant_path)) { visit_child_nodes(node) }
92
+ end
93
+
94
+ def visit_class_node(node)
95
+ with_namespace(constant_name(node.constant_path)) do
96
+ # `class << self` opens a singleton scope for the rest of the body.
97
+ if node.body.is_a?(Prism::SingletonClassNode)
98
+ @singleton_depth += 1
99
+ begin
100
+ visit_child_nodes(node.body)
101
+ ensure
102
+ @singleton_depth -= 1
103
+ end
104
+ else
105
+ visit_child_nodes(node)
106
+ end
107
+ end
108
+ end
109
+
110
+ def visit_singleton_class_node(node)
111
+ @singleton_depth += 1
112
+ begin
113
+ visit_child_nodes(node)
114
+ ensure
115
+ @singleton_depth -= 1
116
+ end
117
+ end
118
+
119
+ def visit_def_node(node)
120
+ @definitions << Definition.new(
121
+ current_owner, node.name.to_s,
122
+ class_method?(node) ? "class" : "instance",
123
+ signature_for(node), @source_path, node.location.start_line
124
+ )
125
+ visit_child_nodes(node)
126
+ end
127
+
128
+ private
129
+
130
+ def with_namespace(name)
131
+ @namespace.push(name)
132
+ yield
133
+ ensure
134
+ @namespace.pop
135
+ end
136
+
137
+ def current_owner
138
+ full = @namespace.reject { |n| n.to_s.strip.empty? }.join("::")
139
+ full.empty? ? nil : full
140
+ end
141
+
142
+ def class_method?(node)
143
+ return true if @singleton_depth.positive?
144
+
145
+ receiver = node.receiver
146
+ receiver.is_a?(Prism::SelfNode) || receiver.is_a?(Prism::ConstantReadNode)
147
+ end
148
+
149
+ def signature_for(node)
150
+ params = node.parameters&.location&.slice || ""
151
+ "#{node.name}(#{params})"
152
+ end
153
+
154
+ def constant_name(node)
155
+ node&.slice || ""
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require "fileutils"
6
+ require_relative "paths"
7
+
8
+ module Gemchat
9
+ # Which projects may run the plugin's index hook on their own.
10
+ #
11
+ # A Bundler plugin is checked-in code that executes for anyone who clones the
12
+ # repository. Running `gemchat index` is much less dangerous than arbitrary
13
+ # code, but it still means one machine's bundle install indexes a path derived
14
+ # from someone else's Gemfile, and it writes to the invoking user's home
15
+ # directory. So the hook asks first.
16
+ #
17
+ # The gate is deliberately low-friction for a project's own author: the first
18
+ # *explicit* gemchat command run inside a project grants trust for that
19
+ # project. Someone who just added `plugin "gemchat"` to their own Gemfile gets
20
+ # auto-indexing with no ceremony. Someone who cloned that repository does not,
21
+ # because they have not run a gemchat command.
22
+ #
23
+ # Records live in GEMCHAT_CONFIG_HOME, not the data root, so wiping the index
24
+ # does not also discard these decisions.
25
+ class Trust
26
+ FILENAME = "trusted.json"
27
+
28
+ class << self
29
+ def path
30
+ File.join(Paths.config_home, FILENAME)
31
+ end
32
+
33
+ def all
34
+ return {} unless File.file?(path)
35
+
36
+ data = JSON.parse(File.read(path))
37
+ data.is_a?(Hash) ? data : {}
38
+ rescue SystemCallError, JSON::ParserError
39
+ # Unreadable or corrupt trust file means "nothing is trusted", which fails
40
+ # closed. Deliberately narrow: a bare rescue also swallowed a NoMethodError
41
+ # from a missing require and reported every project as untrusted, which
42
+ # looks exactly like a working gate and is very hard to see.
43
+ {}
44
+ end
45
+
46
+ def trusted?(dir = Dir.pwd)
47
+ all.key?(expand(dir))
48
+ end
49
+
50
+ def grant!(dir = Dir.pwd, via: "explicit command")
51
+ records = all
52
+ records[expand(dir)] = {"granted_at" => Time.now.utc.iso8601, "via" => via}
53
+ write(records)
54
+ records[expand(dir)]
55
+ end
56
+
57
+ def revoke!(dir = Dir.pwd)
58
+ records = all
59
+ removed = records.delete(expand(dir))
60
+ write(records) if removed
61
+ !removed.nil?
62
+ end
63
+
64
+ def write(records)
65
+ FileUtils.mkdir_p(File.dirname(path))
66
+ File.write(path, JSON.pretty_generate(records))
67
+ end
68
+
69
+ # Project identity is the absolute, symlink-resolved path. Resolving
70
+ # matters: trusting /tmp/x and then arriving at /private/tmp/x on macOS
71
+ # must be the same project, or trust silently never matches.
72
+ def expand(dir)
73
+ File.realpath(dir)
74
+ rescue SystemCallError
75
+ File.expand_path(dir)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemchat
4
+ VERSION = "0.1.0"
5
+ end
data/lib/gemchat.rb ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "json"
6
+ require "time"
7
+ require "rbconfig"
8
+ require "rdoc"
9
+ require "rdoc/ri/driver"
10
+ require "open3"
11
+
12
+ require_relative "gemchat/version"
13
+ require_relative "gemchat/errors"
14
+ require_relative "gemchat/paths"
15
+ require_relative "gemchat/env"
16
+ require_relative "gemchat/ri"
17
+ require_relative "gemchat/symbols"
18
+ require_relative "gemchat/chunker"
19
+ require_relative "gemchat/markdown"
20
+ require_relative "gemchat/prose"
21
+ require_relative "gemchat/stopwords"
22
+ require_relative "gemchat/hosted"
23
+ require_relative "gemchat/models"
24
+ require_relative "gemchat/embedder"
25
+ require_relative "gemchat/store"
26
+ require_relative "gemchat/lockfile"
27
+ require_relative "gemchat/manifest"
28
+ require_relative "gemchat/trust"
29
+ require_relative "gemchat/hook"
30
+ require_relative "gemchat/plugin_index"
31
+ require_relative "gemchat/init"
32
+ require_relative "gemchat/indexer"
33
+ require_relative "gemchat/cli"
34
+
35
+ module Gemchat
36
+ def self.root
37
+ Paths.root
38
+ end
39
+
40
+ def self.config_home
41
+ Paths.config_home
42
+ end
43
+
44
+ def self.version
45
+ Gemchat::VERSION
46
+ end
47
+ end
data/plugins.rb ADDED
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bundler plugin entry point. This file executes on every `bundle install` for
4
+ # anyone who clones a project that declares `plugin "gemchat"`, so it is treated
5
+ # as untrusted input.
6
+ #
7
+ # Three rules, in priority order:
8
+ #
9
+ # 1. Never raise. A plugins.rb that raises is wrapped in MalformattedPlugin and
10
+ # fails the whole install (exit 29, no lockfile written). Nothing here may
11
+ # propagate.
12
+ # 2. Carry no logic. Dual declaration installs two physical copies of gemchat
13
+ # and Bundler 4 writes no PLUGINS lockfile section, so nothing pins the
14
+ # plugin copy to the dependency copy. Requiring the *bundled* copy makes
15
+ # drift harmless: this file only locates and loads.
16
+ # 3. Say one line, or nothing. The hook fires on CI and deploy installs too.
17
+ #
18
+ # This file lives in the gem, never in a consuming project. A project-root
19
+ # plugins.rb is silently never executed.
20
+ #
21
+ # Every Bundler reference is root-scoped with `::`. Bundler loads this file with
22
+ # `load(path, true)`, which wraps it in an anonymous module, and `register_plugin`
23
+ # is mid-flight inside `module Bundler::Plugin` while this runs — so a bare
24
+ # `Bundler::Plugin` here can resolve against the wrapper and raise NameError,
25
+ # which then aborts the install. `::Bundler` cannot be shadowed that way.
26
+ module Bundler
27
+ module Gemchat
28
+ HOST_GEM = "gemchat"
29
+ SUPPORTED = Gem::Requirement.new(">= 0.1.0")
30
+
31
+ class << self
32
+ def auto_index
33
+ # Delegates to the bundled copy. This file deliberately holds no logic:
34
+ # a dual `gem` + `plugin` declaration installs two physical copies and
35
+ # Bundler 4 pins neither to the other, so anything implemented here
36
+ # would exist twice and could disagree with the version the project
37
+ # actually depends on. See §15.4.
38
+ spec = host_spec
39
+ return report("#{HOST_GEM} is not in the bundle") unless spec
40
+ return report("skipped: outside supported range") unless SUPPORTED.satisfied_by?(spec.version)
41
+
42
+ lib = File.join(spec.full_gem_path, "lib")
43
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
44
+ require "gemchat/hook"
45
+ ::Gemchat::Hook.auto_index
46
+ rescue => e
47
+ report "auto-index skipped (#{e.class}: #{e.message})"
48
+ end
49
+
50
+ def host_spec
51
+ @host_spec ||= ::Bundler.load.specs.find { |s| s.name == HOST_GEM }
52
+ end
53
+
54
+ def report(message)
55
+ warn "[gemchat] #{message}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Registration, not execution. A plugins.rb that merely *runs* on load is
62
+ # executed once, during the plugin phase of the very `bundle install` that
63
+ # installed it, and never again: Bundler records which hooks a plugin
64
+ # registered into .bundle/plugin/index, and `hook_plugins` gates every later
65
+ # invocation on that record. Without this line the index shows `hooks:` empty
66
+ # and `bundle add` / `bundle update` are silent no-ops despite the plugin being
67
+ # installed and having printed output once already.
68
+ #
69
+ # The event name must be the string. `Events::GEM_AFTER_INSTALL_ALL` happens to
70
+ # be that same String so it also works, but :after_install_all raises
71
+ # ArgumentError, which register_plugin rewraps as MalformattedPlugin.
72
+ ::Bundler::Plugin.add_hook("after-install-all") do
73
+ Bundler::Gemchat.auto_index
74
+ end
data/sig/gemchat.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Gemchat
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: gemchat
3
+ description: Answer questions about the gems in this project's bundle — their API, configuration, and version-specific behaviour — from a local, offline index. Use when asked how a bundled gem works, what a method does, or how to use a library already in the Gemfile. Triggers on gem names, "how do I use X", method or class names from a dependency, and error messages from a gem.
4
+ ---
5
+
6
+ # gemchat
7
+
8
+ `gemchat` indexes the documentation of the gems in **this project's own
9
+ `Gemfile.lock`** and answers questions about them offline. It is a local tool: no
10
+ account, no API key, nothing leaves the machine.
11
+
12
+ ## Reach for this when
13
+
14
+ - Someone asks how a gem **in this bundle** works, or what a method does.
15
+ - An error message came from a dependency and you need the surrounding API.
16
+ - You need the signature or the configuration options of something installed.
17
+
18
+ ## Do not reach for this when
19
+
20
+ - The question is about **this application's** runtime — routes, schema, job
21
+ state, request logs. That is Rails Hyperdrive's job, not a documentation index.
22
+ - The gem is **not in the bundle**. gemchat indexes `Gemfile.lock`; a gem you are
23
+ merely considering is not there.
24
+
25
+ ## Pick the right command
26
+
27
+ This is the part worth getting right, because the two search commands fail in
28
+ **opposite** directions and using the wrong one returns nothing at all.
29
+
30
+ | Question shape | Command | Why |
31
+ |---|---|---|
32
+ | An exact identifier — `Rake::FileList#exclude`, an error string, a flag | `gemchat search` | Instant, no model needed, and exact. |
33
+ | A natural-language question — "how do I list every task" | `gemchat query` | Needs vectors; use after `embed`. |
34
+ | A paraphrase you want only the vector arm for | `gemchat vsearch` | Same requirement. |
35
+
36
+ **`gemchat search` does not do keyword search.** It matches exact phrases. A
37
+ question like *"how do I list all the available tasks"* returns **no results**,
38
+ because those words do not appear together in any chunk. That is expected
39
+ behaviour, not a broken index — reach for `query` instead.
40
+
41
+ `gemchat query` takes a leading type that routes it:
42
+
43
+ ```sh
44
+ gemchat query "how do I list every task" # both engines, fused (default)
45
+ gemchat query "lex: Rake::Task#enhance" # BM25 only
46
+ gemchat query "vec: stop a process on a signal" # vectors only
47
+ ```
48
+
49
+ `lex:` never loads a model, so it is the cheap choice for a symbol.
50
+
51
+ ## Before searching, run `gemchat status`
52
+
53
+ It is one line each, and it tells you whether an answer is even possible:
54
+
55
+ ```
56
+ index: 5 gems · 1095 chunks # what is indexed
57
+ manifest: up to date # index matches the current lockfile
58
+ vectors: not downloaded (146MB) → gemchat embed
59
+ hosted: not configured → gemchat.org/settings
60
+ ```
61
+
62
+ - `vectors: not downloaded` means `query` and `vsearch` **cannot answer yet**.
63
+ They do not download anything themselves; `gemchat embed` is the explicit step.
64
+ - `manifest: stale` means the lockfile has moved since the last index. Re-run
65
+ `gemchat index`, or answers may be about a version that is no longer installed.
66
+
67
+ ## Cost
68
+
69
+ - `gemchat search` — instant, free, no model on disk.
70
+ - `gemchat embed` — one-time, downloads a 146MB model, then embeds the index.
71
+ Every later semantic search is local and fast.
72
+ - If the model is not downloaded, `query` degrades to BM25 and says so on stderr.
73
+ It does not fail silently and it does not download behind your back.
74
+
75
+ ## Reading a result
76
+
77
+ Each result names its source, and that changes how you should treat it:
78
+
79
+ ```
80
+ 1. [rake 13.4.2 ri ] Rake::FileList#exclude
81
+ exclude(*patterns, &block) (lib/rake/file_list.rb:150)
82
+ ```
83
+
84
+ - `ri` is API reference generated from the installed gem's own source. The
85
+ `path:line` is the real definition site — open it before asserting behaviour.
86
+ - `readme`, `guides` and `doc` are the gem's hand-written prose, split by
87
+ heading. Use these for *why* and *when*, not for signatures.
88
+
89
+ If `path:line` is missing on an `ri` hit, the definition is not recoverable by
90
+ parsing (a C extension, `define_method`, or a macro). Say so rather than
91
+ guessing a file.
92
+
93
+ ## Version fidelity
94
+
95
+ The index is built from `Gemfile.lock`, so local answers are about **the versions
96
+ actually installed** — usually what you want. Check `manifest: up to date` before
97
+ relying on that; if it is stale, the answer may describe a version that is no
98
+ longer there.
99
+
100
+ ### Hosted backend
101
+
102
+ `gemchat query --hosted` asks gemchat.org instead. It is opt-in and **never
103
+ happens automatically** — there is no route from a failed or unconfigured hosted
104
+ query to a local answer, so you will get an error rather than a local result you
105
+ did not ask for. Do not retry it locally and present that as the answer.
106
+
107
+ It sends this lockfile's exact gem versions, and the response says which ones it
108
+ could scope to:
109
+
110
+ ```
111
+ note: (scoped to 13.4.2; no per-version index for sidekiq)
112
+ ```
113
+
114
+ Report that note. A gem with no per-version index on the server is answered from
115
+ its full docs, which may not be the pinned release — so a scoped note is fidelity,
116
+ and an absent one is not.
117
+
118
+ Two things that do not exist on the hosted side:
119
+
120
+ - **`lex:` has no hosted equivalent.** It is BM25, which is local-only.
121
+ `gemchat query --hosted lex: …` refuses and points at `gemchat search`. Use
122
+ `gemchat search` for a symbol.
123
+ - **`search` and `vsearch` are local-only**, whatever the backend is configured.
124
+
125
+ If the server does not have a gem you named, the result says so and points at
126
+ `--auto-index`, which asks gemchat.org to index it. That spends the caller's
127
+ quota, so it is opt-in and the message is a suggestion, not something to pass
128
+ through on your own.
129
+
130
+ ## Invocation
131
+
132
+ Use `bundle exec gemchat` when gemchat is a development dependency in this
133
+ project's Gemfile (which is what `gemchat init` sets up), so you get the project's
134
+ bundle. Use bare `gemchat` when it is installed globally.
135
+
136
+ ## When the index is wrong
137
+
138
+ - `gemchat reindex` discards the index and the generated ri, then builds it
139
+ again. Use it when `status` reports a stale manifest, or after upgrading
140
+ gemchat, whose store format is version-gated and will refuse to open a store
141
+ it did not write.
142
+ - `gemchat version` prints the version, which is worth quoting when a search
143
+ behaves unexpectedly.
144
+
145
+ ## Setup
146
+
147
+ `gemchat init` is the only command that modifies a Gemfile. It adds gemchat as a
148
+ development dependency and a Bundler plugin, and grants trust for this project.
149
+ After that, any `bundle install`, `bundle add` or `bundle update` that moves the
150
+ lockfile re-indexes automatically and prints one line. Anything else is silent.
151
+
152
+ `gemchat init --hosted` additionally records the hosted backend as this
153
+ project's default, so later `gemchat query` calls use it without a flag. It still
154
+ needs a key: `GEMCHAT_API_KEY`, or a `credentials` file under the gemchat config
155
+ home. `gemchat status` says which of those is in play.
156
+
157
+ If the project has no `Gemfile.lock`, or the gems are not installed, `index`
158
+ cannot work — those gems have to be on disk to be documented.
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gemchat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Saroj Maharjan (zoras)
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sqlite3
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rllama
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: prism
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rdoc
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '6.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '6.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: json
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '2.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '2.0'
82
+ description: |
83
+ gemchat indexes the documentation for the gems in your project's own bundle and
84
+ serves BM25 search over it from the command line, with no account, no API key and
85
+ no network.
86
+
87
+ It is a local tool, not the hosted gemchat.org service. An optional mode can defer
88
+ to gemchat.org if you would rather not keep a local index.
89
+
90
+ Direct dependencies are indexed by default and ri documentation is generated
91
+ locally, because Bundler installs gems without documentation. Results carry the
92
+ exact locked version and real API signatures, which is the point: an agent can
93
+ look up how to call a library without reading a 1500-line lockfile into context.
94
+ email:
95
+ - zoras@users.noreply.github.com
96
+ executables:
97
+ - gemchat
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - AGENTS.md
102
+ - CHANGELOG.md
103
+ - CODE_OF_CONDUCT.md
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - docs/plans/gemchat-local-bundle-index.md
108
+ - exe/gemchat
109
+ - lib/gemchat.rb
110
+ - lib/gemchat/chunker.rb
111
+ - lib/gemchat/cli.rb
112
+ - lib/gemchat/embedder.rb
113
+ - lib/gemchat/env.rb
114
+ - lib/gemchat/errors.rb
115
+ - lib/gemchat/hook.rb
116
+ - lib/gemchat/hosted.rb
117
+ - lib/gemchat/indexer.rb
118
+ - lib/gemchat/init.rb
119
+ - lib/gemchat/lockfile.rb
120
+ - lib/gemchat/manifest.rb
121
+ - lib/gemchat/markdown.rb
122
+ - lib/gemchat/models.rb
123
+ - lib/gemchat/paths.rb
124
+ - lib/gemchat/plugin_index.rb
125
+ - lib/gemchat/prose.rb
126
+ - lib/gemchat/ri.rb
127
+ - lib/gemchat/stopwords.rb
128
+ - lib/gemchat/store.rb
129
+ - lib/gemchat/symbols.rb
130
+ - lib/gemchat/trust.rb
131
+ - lib/gemchat/version.rb
132
+ - plugins.rb
133
+ - sig/gemchat.rbs
134
+ - skills/gemchat/SKILL.md
135
+ homepage: https://github.com/zoras/gemchat
136
+ licenses:
137
+ - MIT
138
+ metadata:
139
+ homepage_uri: https://github.com/zoras/gemchat
140
+ source_code_uri: https://github.com/zoras/gemchat
141
+ changelog_uri: https://github.com/zoras/gemchat/blob/main/CHANGELOG.md
142
+ rubygems_mfa_required: 'true'
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: 3.2.0
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 4.0.22
158
+ specification_version: 4
159
+ summary: Local, offline documentation index for the gems in your own Gemfile.lock
160
+ test_files: []