agent-context 0.0.2 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52c1839c403ab5a9b54f0ea7604d792fd284eb19c789a5f9d2afe356e6c7d992
4
- data.tar.gz: 1f20c9f320200d4f2e70b838ba1a478ff69831924df81ce891115e3abfc6896f
3
+ metadata.gz: 5772240bc3bd7661f586cea5f942b21be8c6700dd01df4e287ccf88be7360885
4
+ data.tar.gz: 6e467af1cb56e030210770065eb566bdbcc0b4c17fcc171555096d69482fb081
5
5
  SHA512:
6
- metadata.gz: d942db85db0f5a98a0feaf136cd43ca270f3ce29db40f1f63a4df899c9394194fba691b588caf6191d2b653b064cc16d18362abbf66786d9e6f3b42f6f30503f
7
- data.tar.gz: feb0ba1a782cb034dcabfb8fe50767f4ff490b7ede56fada3b34a7124a659b8ce76aa6e14efca0a2ebf6ec456ebaa8fd2fb2b64ad2d73fe65b5c115a3c95507f
6
+ metadata.gz: ebd792d272ddb3619194bf3ddbd8858a13e81446e32a96c0fbd8a6314609e647ca0ffc7d73423abcd0b151d688b5284df36084b49e2d404763ba5e95bbe40a6e
7
+ data.tar.gz: 8025c2fa9fa4e25d7b6c87c520e34234f0b1ee2114ecb244d1ede213adfc406b49b4e084a4ac32e3f721b72498a890fb5355662684da2265dd30f8e6b78a3ddc
checksums.yaml.gz.sig CHANGED
Binary file
data/agent.md ADDED
@@ -0,0 +1,37 @@
1
+ # Agent
2
+
3
+ ## Context
4
+
5
+ This section provides links to documentation from installed packages. It is automatically generated and may be updated by running `bake agent:context:install`.
6
+
7
+ ### decode
8
+
9
+ Code analysis for documentation generation.
10
+
11
+ #### [Getting Started with Decode](.context/decode/getting-started.md)
12
+
13
+ The Decode gem provides programmatic access to Ruby code structure and metadata. It can parse Ruby files and extract definitions, comments, and documentation pragmas, enabling code analysis, docume...
14
+
15
+ #### [Documentation Coverage](.context/decode/coverage.md)
16
+
17
+ This guide explains how to test and monitor documentation coverage in your Ruby projects using the Decode gem's built-in bake tasks.
18
+
19
+ #### [Ruby Documentation](.context/decode/ruby-documentation.md)
20
+
21
+ This guide covers documentation practices and pragmas supported by the Decode gem for documenting Ruby code. These pragmas provide structured documentation that can be parsed and used to generate A...
22
+
23
+ ### sus
24
+
25
+ A fast and scalable test runner.
26
+
27
+ #### [Using Sus Testing Framework](.context/sus/usage.md)
28
+
29
+ Sus is a modern Ruby testing framework that provides a clean, BDD-style syntax for writing tests. It's designed to be fast, simple, and expressive.
30
+
31
+ #### [Mocking](.context/sus/mocking.md)
32
+
33
+ There are two types of mocking in sus: `receive` and `mock`. The `receive` matcher is a subset of full mocking and is used to set expectations on method calls, while `mock` can be used to replace m...
34
+
35
+ #### [Shared Test Behaviors and Fixtures](.context/sus/shared.md)
36
+
37
+ Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files.
@@ -4,34 +4,35 @@
4
4
  # Copyright, 2025, by Shopify Inc.
5
5
  # Copyright, 2025, by Samuel Williams.
6
6
 
7
- require_relative "../../lib/agent/context/helper"
7
+ require_relative "../../lib/agent/context/installer"
8
+ require_relative "../../lib/agent/context/index"
8
9
 
9
10
  include Agent::Context
10
11
 
11
12
  def initialize(context)
12
13
  super(context)
13
14
 
14
- @helper = Helper.new(root: context.root)
15
+ @installer = Installer.new(root: context.root)
15
16
  end
16
17
 
17
- attr :helper
18
+ attr :installer
18
19
 
19
20
  # List all gems that have context available.
20
21
  # @parameter gem [String] Optional specific gem name to list context files for.
21
22
  def list(gem: nil)
22
23
  if gem
23
- files = @helper.list_context_files(gem)
24
+ files = @installer.list_context_files(gem)
24
25
  if files
25
26
  puts "Context files for gem '#{gem}':"
26
27
  files.each do |file|
27
- relative_path = Pathname.new(file).relative_path_from(Pathname.new(@helper.find_gem_with_context(gem)[:path]))
28
+ relative_path = Pathname.new(file).relative_path_from(Pathname.new(@installer.find_gem_with_context(gem)[:path]))
28
29
  puts " #{relative_path}"
29
30
  end
30
31
  else
31
32
  puts "No context found for gem '#{gem}'"
32
33
  end
33
34
  else
34
- gems = @helper.find_gems_with_context
35
+ gems = @installer.find_gems_with_context
35
36
  if gems.any?
36
37
  puts "Gems with context available:"
37
38
  gems.each do |gem_info|
@@ -47,7 +48,7 @@ end
47
48
  # @parameter gem [String] The gem name.
48
49
  # @parameter file [String] The context file name.
49
50
  def show(gem:, file:)
50
- content = @helper.show_context_file(gem, file)
51
+ content = @installer.show_context_file(gem, file)
51
52
  if content
52
53
  puts content
53
54
  else
@@ -59,18 +60,29 @@ end
59
60
  # @parameter gem [String] Optional specific gem name to install context from.
60
61
  def install(gem: nil)
61
62
  if gem
62
- if @helper.install_gem_context(gem)
63
+ if @installer.install_gem_context(gem)
63
64
  puts "Installed context from gem '#{gem}'"
64
65
  else
65
66
  puts "No context found for gem '#{gem}'"
66
67
  end
67
68
  else
68
- installed = @helper.install_all_context
69
+ installed = @installer.install_all_context
69
70
  if installed.any?
70
71
  puts "Installed context from #{installed.length} gems:"
71
- installed.each { |gem_name| puts " #{gem_name}" }
72
+ installed.each {|gem_name| puts " #{gem_name}"}
72
73
  else
73
74
  puts "No gems with context found"
74
75
  end
75
76
  end
76
- end
77
+
78
+ # Update agent.md after installing context
79
+ index = Agent::Context::Index.new(@installer.context_path)
80
+ index.update_agent_md
81
+ end
82
+
83
+ # Update or create AGENT.md in the project root with context section
84
+ # This follows the AGENT.md specification for agentic coding tools
85
+ def agent_md(path = "agent.md")
86
+ index = Agent::Context::Index.new(@helper.context_path)
87
+ index.update_agent_md(path)
88
+ end
data/context/usage.md ADDED
@@ -0,0 +1,173 @@
1
+ # Usage Guide
2
+
3
+ ## What is agent-context?
4
+
5
+ `agent-context` is a tool that helps you discover and install contextual information from Ruby gems for AI agents. Gems can provide additional documentation, examples, and guidance in a `context/` directory.
6
+
7
+ ## Quick Commands
8
+
9
+ ```bash
10
+ # See what context is available
11
+ bake agent:context:list
12
+
13
+ # Install all available context
14
+ bake agent:context:install
15
+
16
+ # Install context from a specific gem
17
+ bake agent:context:install --gem async
18
+
19
+ # See what context files a gem provides
20
+ bake agent:context:list --gem async
21
+
22
+ # View a specific context file
23
+ bake agent:context:show --gem async --file thread-safety
24
+ ```
25
+
26
+ ## Understanding context/ vs .context/
27
+
28
+ **Important distinction:**
29
+ - **`context/`** (no dot) = Directory in gems that contains context files to share.
30
+ - **`.context/`** (with dot) = Directory in your project where context gets installed.
31
+
32
+ ### What happens when you install context?
33
+
34
+ When you run `bake agent:context:install`, the tool:
35
+
36
+ 1. Scans all installed gems for `context/` directories (in the gem's root).
37
+ 2. Creates a `.context/` directory in your current project.
38
+ 3. Copies context files organized by gem name.
39
+
40
+ For example:
41
+ ```
42
+ your-project/
43
+ ├── .context/ # ← Installed context (with dot)
44
+ │ ├── async/ # ← From the 'async' gem's context/ directory
45
+ │ │ ├── thread-safety.md
46
+ │ │ └── performance.md
47
+ │ └── rack/ # ← From the 'rack' gem's context/ directory
48
+ │ └── middleware.md
49
+ ├── lib/
50
+ └── Gemfile
51
+ ```
52
+
53
+ Meanwhile, in the gems themselves:
54
+ ```
55
+ async-gem/
56
+ ├── context/ # ← Source context (no dot)
57
+ │ ├── thread-safety.md
58
+ │ └── performance.md
59
+ ├── lib/
60
+ └── async.gemspec
61
+ ```
62
+
63
+ ## Using Context (For Gem Users)
64
+
65
+ ### Why use this?
66
+
67
+ - **Discover hidden documentation** that gems provide.
68
+ - **Get practical examples** and guidance.
69
+ - **Understand best practices** from gem authors.
70
+ - **Access migration guides** and troubleshooting tips.
71
+
72
+ ### Key Points for Users
73
+
74
+ - Run `bake agent:context:install` to copy context to `.context/` (with dot).
75
+ - The `.context/` directory is where installed context lives in your project.
76
+ - Don't edit files in `.context/` - they get completely replaced when you reinstall.
77
+
78
+ ## Providing Context (For Gem Authors)
79
+
80
+ ### How to provide context in your gem
81
+
82
+ #### 1. Create a `context/` directory
83
+
84
+ In your gem's root directory, create a `context/` folder (no dot):
85
+
86
+ ```
87
+ your-gem/
88
+ ├── context/ # ← Source context (no dot) - this is what you create
89
+ │ ├── getting-started.md
90
+ │ ├── configuration.md
91
+ │ └── troubleshooting.md
92
+ ├── lib/
93
+ └── your-gem.gemspec
94
+ ```
95
+
96
+ **Important:** This is different from `.context/` (with dot) which is where context gets installed in user projects.
97
+
98
+ #### 2. Add context files
99
+
100
+ Create files with helpful information for users of your gem. Common types include:
101
+
102
+ - **getting-started.md** - Quick start guide for using your gem.
103
+ - **configuration.md** - Configuration options and examples.
104
+ - **troubleshooting.md** - Common issues and solutions.
105
+ - **migration.md** - Migration guides between versions.
106
+ - **performance.md** - Performance tips and best practices.
107
+ - **security.md** - Security considerations.
108
+
109
+ **Focus on the agent experience:** These files should help AI agents understand how to use your gem effectively, not document your gem's internal APIs.
110
+
111
+ #### 3. Document your context
112
+
113
+ Add a section to your gem's README:
114
+
115
+ ```markdown
116
+ ## Context
117
+
118
+ This gem provides additional context files that can be installed using `bake agent:context:install`.
119
+
120
+ Available context files:
121
+ - `getting-started.md` - Quick start guide.
122
+ - `configuration.md` - Configuration options.
123
+ - `troubleshooting.md` - Common issues and solutions.
124
+ ```
125
+
126
+ #### 4. File format and content guidelines
127
+
128
+ Context files can be in any format, but `.md` is commonly used for documentation. The content should be:
129
+
130
+ - **Practical** - Include real examples and working code.
131
+ - **Focused** - One topic per file.
132
+ - **Clear** - Easy to understand and follow.
133
+ - **Actionable** - Provide specific guidance and next steps.
134
+ - **Agent-focused** - Help AI agents understand how to use your gem effectively.
135
+
136
+ ### Key Points for Gem Authors
137
+
138
+ - Create a `context/` directory (no dot) in your gem's root.
139
+ - Put helpful guides for users of your gem there.
140
+ - Focus on practical usage, not API documentation.
141
+
142
+ ## Example Context Files
143
+
144
+ For examples of well-structured context files, see the existing files in this directory:
145
+ - `usage.md` - Shows how to use the tool (this file).
146
+ - `examples.md` - Demonstrates practical usage scenarios.
147
+
148
+ ## Key Differences from API Documentation
149
+
150
+ Context files are NOT the same as API documentation:
151
+
152
+ - **Context files**: Help agents accomplish tasks ("How do I configure authentication?").
153
+ - **API documentation**: Document methods and classes ("Method `authenticate` returns Boolean").
154
+
155
+ Context files should answer questions like:
156
+ - "How do I get started?".
157
+ - "How do I configure this for production?".
158
+ - "What do I do when X goes wrong?".
159
+ - "How do I migrate from version Y to Z?".
160
+
161
+ ## Testing Your Context
162
+
163
+ Before publishing, test your context files:
164
+
165
+ 1. Have an AI agent try to follow your getting-started guide.
166
+ 2. Check that all code examples actually work.
167
+ 3. Ensure the files are focused and don't try to cover too much.
168
+ 4. Verify that they complement rather than duplicate your main documentation.
169
+
170
+ ## Summary
171
+
172
+ - **`context/`** = source (in gems).
173
+ - **`.context/`** = destination (in your project).
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+ # Copyright, 2025, by Shopify Inc.
6
+
7
+ require_relative "version"
8
+ require "fileutils"
9
+ require "yaml"
10
+
11
+ # @namespace
12
+ module Agent
13
+ # @namespace
14
+ module Context
15
+ # Represents an index for managing and generating agent.md files from context files.
16
+ #
17
+ # This class provides functionality to update or create AGENT.md files following
18
+ # the AGENT.md specification for agentic coding tools. It can parse existing
19
+ # agent.md files, update the context section, and generate new files when needed.
20
+ class Index
21
+ # Initialize a new index instance.
22
+ # @parameter context_path [String] The path to the context directory (default: ".context").
23
+ def initialize(context_path = ".context")
24
+ @context_path = context_path
25
+ end
26
+
27
+ attr :context_path
28
+
29
+ # Update or create an AGENT.md file in the project root with context section
30
+ # This follows the AGENT.md specification for agentic coding tools
31
+ def update_agent_md(agent_md_path = "agent.md")
32
+ context_content = generate_context_section
33
+
34
+ if File.exist?(agent_md_path)
35
+ update_existing_agent_md(agent_md_path, context_content)
36
+ else
37
+ create_new_agent_md(agent_md_path, context_content)
38
+ end
39
+
40
+ Console.debug("Updated agent.md: #{agent_md_path}")
41
+ end
42
+
43
+ # Generate just the context section content (without top-level headers)
44
+ def generate_context_section
45
+ sections = []
46
+
47
+ sections << "This section provides links to documentation from installed packages. It is automatically generated and may be updated by running `bake agent:context:install`."
48
+ sections << ""
49
+
50
+ gem_contexts = collect_gem_contexts
51
+
52
+ if gem_contexts.empty?
53
+ sections << "No context files found. Run `bake agent:context:install` to install context from gems."
54
+ sections << ""
55
+ else
56
+ gem_contexts.each do |gem_name, files|
57
+ sections << "### #{gem_name}"
58
+ sections << ""
59
+
60
+ # Get gem directory and load index
61
+ gem_directory = File.join(@context_path, gem_name)
62
+ index = load_gem_index(gem_name, gem_directory)
63
+
64
+ # Add gem description from index
65
+ if index["description"]
66
+ sections << index["description"]
67
+ sections << ""
68
+ end
69
+
70
+ # Use files from index if available, otherwise fall back to parsing
71
+ if index["files"] && !index["files"].empty?
72
+ index["files"].each do |file_info|
73
+ sections << "#### [#{file_info['title']}](.context/#{gem_name}/#{file_info['path']})"
74
+ sections << ""
75
+ sections << file_info["description"] if file_info["description"] && !file_info["description"].empty?
76
+ sections << ""
77
+ end
78
+ else
79
+ # Fallback to parsing files directly
80
+ files.each do |file_path|
81
+ if File.exist?(file_path)
82
+ title, description = extract_content(file_path)
83
+ relative_path = file_path.sub("#{@context_path}/", "")
84
+ sections << "#### [#{title}](.context/#{relative_path})"
85
+ sections << ""
86
+ sections << description if description && !description.empty?
87
+ sections << ""
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ sections.join("\n")
95
+ end
96
+
97
+ private
98
+
99
+ def update_existing_agent_md(agent_md_path, context_content)
100
+ content = File.read(agent_md_path)
101
+
102
+ # Find the # Agent heading
103
+ agent_heading_line = find_agent_heading_line(content)
104
+
105
+ if agent_heading_line
106
+ # Find or create the ## Context section
107
+ context_section = find_context_section_under_agent(content, agent_heading_line)
108
+
109
+ if context_section
110
+ # Replace existing context section
111
+ updated_content = replace_context_section(content, context_section, context_content)
112
+ else
113
+ # Insert new context section after agent heading
114
+ updated_content = insert_context_section_after_agent(content, agent_heading_line, context_content)
115
+ end
116
+ else
117
+ # No # Agent heading found, prepend it with context
118
+ updated_content = prepend_agent_with_context(content, context_content)
119
+ end
120
+
121
+ # Write the updated content back to file
122
+ File.write(agent_md_path, updated_content)
123
+ end
124
+
125
+ def create_new_agent_md(agent_md_path, context_content)
126
+ content = [
127
+ "# Agent",
128
+ "",
129
+ "## Context",
130
+ "",
131
+ context_content,
132
+ ].join("\n")
133
+ File.write(agent_md_path, content)
134
+ end
135
+
136
+ def find_agent_heading_line(content)
137
+ lines = content.lines
138
+ lines.each_with_index do |line, index|
139
+ if line.strip.start_with?("# ") && line.strip.downcase == "# agent"
140
+ return index
141
+ end
142
+ end
143
+ nil
144
+ end
145
+
146
+ def find_context_section_under_agent(content, agent_line_index)
147
+ lines = content.lines
148
+
149
+ # Look for ## Context after the agent heading
150
+ (agent_line_index + 1).upto(lines.length - 1) do |index|
151
+ line = lines[index]
152
+ if line.strip == "## Context"
153
+ return index
154
+ elsif line.strip.start_with?("# ") && line.strip != "# Agent"
155
+ # We've hit another top-level heading, stop searching
156
+ break
157
+ end
158
+ end
159
+
160
+ nil
161
+ end
162
+
163
+ def replace_context_section(content, context_line_index, context_content)
164
+ lines = content.lines
165
+
166
+ # Find the end of the context section
167
+ end_index = find_section_end(lines, context_line_index, 2)
168
+
169
+ # Build the new content
170
+ new_lines = []
171
+ new_lines.concat(lines[0...context_line_index])
172
+ new_lines << "## Context\n"
173
+ new_lines << "\n"
174
+ new_lines.concat(context_content.lines)
175
+ new_lines.concat(lines[end_index..-1])
176
+
177
+ new_lines.join
178
+ end
179
+
180
+ def insert_context_section_after_agent(content, agent_line_index, context_content)
181
+ lines = content.lines
182
+
183
+ # Build the new content
184
+ new_lines = []
185
+ new_lines.concat(lines[0..agent_line_index])
186
+ new_lines << "\n"
187
+ new_lines << "## Context\n"
188
+ new_lines << "\n"
189
+ new_lines.concat(context_content.lines)
190
+ new_lines.concat(lines[agent_line_index + 1..-1])
191
+
192
+ new_lines.join
193
+ end
194
+
195
+ def prepend_agent_with_context(content, context_content)
196
+ agent_context = [
197
+ "# Agent",
198
+ "",
199
+ "## Context",
200
+ "",
201
+ context_content,
202
+ ""
203
+ ].join("\n")
204
+ agent_context + content
205
+ end
206
+
207
+ def find_section_end(lines, start_index, heading_level)
208
+ index = start_index + 1
209
+
210
+ while index < lines.length
211
+ line = lines[index]
212
+
213
+ if line.strip.start_with?("#")
214
+ level = line.strip.match(/^(#+)/)[1].length
215
+ if level <= heading_level
216
+ break
217
+ end
218
+ end
219
+
220
+ index += 1
221
+ end
222
+
223
+ index
224
+ end
225
+
226
+ def collect_gem_contexts
227
+ gem_contexts = {}
228
+
229
+ return gem_contexts unless Dir.exist?(@context_path)
230
+
231
+ Dir.glob(File.join(@context_path, "*")).each do |gem_directory|
232
+ next unless File.directory?(gem_directory)
233
+ gem_name = File.basename(gem_directory)
234
+
235
+ markdown_files = Dir.glob(File.join(gem_directory, "**", "*.md")).sort
236
+ gem_contexts[gem_name] = markdown_files if markdown_files.any?
237
+ end
238
+
239
+ gem_contexts
240
+ end
241
+
242
+ # Load a gem's index file
243
+ def load_gem_index(gem_name, gem_directory)
244
+ index_path = File.join(gem_directory, "index.yaml")
245
+
246
+ if File.exist?(index_path)
247
+ YAML.load_file(index_path)
248
+ else
249
+ # Return a fallback index if no index.yaml exists
250
+ {
251
+ "description" => "Context files for #{gem_name}",
252
+ "files" => []
253
+ }
254
+ end
255
+ rescue => error
256
+ Console.debug("Error loading index for #{gem_name}: #{error.message}")
257
+ # Return a fallback index
258
+ {
259
+ "description" => "Context files for #{gem_name}",
260
+ "files" => []
261
+ }
262
+ end
263
+
264
+ def extract_content(file_path)
265
+ content = File.read(file_path)
266
+ lines = content.lines.map(&:strip)
267
+
268
+ title = extract_title(lines)
269
+ description = extract_description(lines)
270
+
271
+ [title, description]
272
+ end
273
+
274
+ def extract_title(lines)
275
+ # Look for the first markdown header
276
+ header_line = lines.find {|line| line.start_with?("#")}
277
+ if header_line
278
+ # Remove markdown header syntax and clean up
279
+ header_line.sub(/^#+\s*/, "").strip
280
+ else
281
+ # If no header found, use a default
282
+ "Documentation"
283
+ end
284
+ end
285
+
286
+ def extract_description(lines)
287
+ # Skip empty lines and headers to find the first paragraph
288
+ content_start = false
289
+ description_lines = []
290
+
291
+ lines.each do |line|
292
+ # Skip headers
293
+ next if line.start_with?("#")
294
+
295
+ # Skip empty lines until we find content
296
+ if !content_start && line.empty?
297
+ next
298
+ end
299
+
300
+ # Mark that we've found content
301
+ content_start = true
302
+
303
+ # If we hit an empty line after finding content, we've reached the end of the first paragraph
304
+ if line.empty?
305
+ break
306
+ end
307
+
308
+ description_lines << line
309
+ end
310
+
311
+ # Join the lines and truncate if too long
312
+ description = description_lines.join(" ").strip
313
+ if description.length > 197
314
+ description = description[0..196] + "..."
315
+ end
316
+
317
+ description
318
+ end
319
+ end
320
+ end
321
+ end