agent-context 0.0.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4dd25415ecb31136843810a6455fa0594d134b4fe8c926a8b43f75b18be232d2
4
- data.tar.gz: bf2d14ca245188f5a878ae6761b5092d69c0c1fc80c90d3bccd5dc16a61459ec
3
+ metadata.gz: 71c3555039c6fe5ae6c08d31a01b7ad69fdbc37d5dc13ddedbd9066fbd42a202
4
+ data.tar.gz: 496c8d37bc994834ca49328976f53cedef923c1aafd76073eac1218e27322579
5
5
  SHA512:
6
- metadata.gz: 5a8e4b97106b8eda55a54ff66a471c1cb272d987bb9359620a7722cd410471abaf7ac0c5b4ebc045410f5a4fc85ff687133a2eaec11ac53873f3fe2c2d7f8877
7
- data.tar.gz: '002169fa66a2310b8e3c05eb7735165255136141cdb5daf6ad118daf69bb5c283e3ef6dc8ad3a8f39f4c37fa60cdc13f88d0d4a4fca713d85e3bb0652607b8a1'
6
+ metadata.gz: 35e2df052388b0bc29c373ec4fb287ed595cb4de3aa8e7178e80917be78937e0ee7477d11a75c8403c104a0b328eb3307c75d91ec102616f663fbbe64dbe7ffc
7
+ data.tar.gz: 5597bce947893b800f481c9df0ae89a70821e162593ca437116de60c1f58b981847d66c6ff80e90299385aed9e56d369d3d9b714ab794089bab1a65350d3872b
checksums.yaml.gz.sig CHANGED
Binary file
data/agent.md ADDED
@@ -0,0 +1,37 @@
1
+ # Agent
2
+
3
+ ## Context
4
+
5
+ Context files from installed gems providing documentation and guidance for AI agents.
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.
@@ -2,35 +2,37 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2025, by Shopify Inc.
5
+ # Copyright, 2025, by Samuel Williams.
5
6
 
6
- require_relative "../../lib/agent/context/helper"
7
+ require_relative "../../lib/agent/context/installer"
8
+ require_relative "../../lib/agent/context/index"
7
9
 
8
10
  include Agent::Context
9
11
 
10
12
  def initialize(context)
11
13
  super(context)
12
14
 
13
- @helper = Helper.new(root: context.root)
15
+ @installer = Installer.new(root: context.root)
14
16
  end
15
17
 
16
- attr :helper
18
+ attr :installer
17
19
 
18
20
  # List all gems that have context available.
19
21
  # @parameter gem [String] Optional specific gem name to list context files for.
20
22
  def list(gem: nil)
21
23
  if gem
22
- files = @helper.list_context_files(gem)
24
+ files = @installer.list_context_files(gem)
23
25
  if files
24
26
  puts "Context files for gem '#{gem}':"
25
27
  files.each do |file|
26
- 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]))
27
29
  puts " #{relative_path}"
28
30
  end
29
31
  else
30
32
  puts "No context found for gem '#{gem}'"
31
33
  end
32
34
  else
33
- gems = @helper.find_gems_with_context
35
+ gems = @installer.find_gems_with_context
34
36
  if gems.any?
35
37
  puts "Gems with context available:"
36
38
  gems.each do |gem_info|
@@ -46,7 +48,7 @@ end
46
48
  # @parameter gem [String] The gem name.
47
49
  # @parameter file [String] The context file name.
48
50
  def show(gem:, file:)
49
- content = @helper.show_context_file(gem, file)
51
+ content = @installer.show_context_file(gem, file)
50
52
  if content
51
53
  puts content
52
54
  else
@@ -58,13 +60,13 @@ end
58
60
  # @parameter gem [String] Optional specific gem name to install context from.
59
61
  def install(gem: nil)
60
62
  if gem
61
- if @helper.install_gem_context(gem)
63
+ if @installer.install_gem_context(gem)
62
64
  puts "Installed context from gem '#{gem}'"
63
65
  else
64
66
  puts "No context found for gem '#{gem}'"
65
67
  end
66
68
  else
67
- installed = @helper.install_all_context
69
+ installed = @installer.install_all_context
68
70
  if installed.any?
69
71
  puts "Installed context from #{installed.length} gems:"
70
72
  installed.each { |gem_name| puts " #{gem_name}" }
@@ -72,4 +74,15 @@ def install(gem: nil)
72
74
  puts "No gems with context found"
73
75
  end
74
76
  end
75
- 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,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative "version"
7
+ require "fileutils"
8
+ require "markly"
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 << "Context files from installed gems providing documentation and guidance for AI agents."
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
+ ""
133
+ ].join("\n")
134
+ File.write(agent_md_path, content)
135
+ end
136
+
137
+ def find_agent_heading_line(content)
138
+ lines = content.lines
139
+ lines.each_with_index do |line, index|
140
+ if line.strip.start_with?("# ") && line.strip.downcase == "# agent"
141
+ return index
142
+ end
143
+ end
144
+ nil
145
+ end
146
+
147
+ def find_context_section_under_agent(content, agent_line_index)
148
+ lines = content.lines
149
+
150
+ # Look for ## Context after the agent heading
151
+ (agent_line_index + 1).upto(lines.length - 1) do |index|
152
+ line = lines[index]
153
+ if line.strip == "## Context"
154
+ return index
155
+ elsif line.strip.start_with?("# ") && line.strip != "# Agent"
156
+ # We've hit another top-level heading, stop searching
157
+ break
158
+ end
159
+ end
160
+
161
+ nil
162
+ end
163
+
164
+ def replace_context_section(content, context_line_index, context_content)
165
+ lines = content.lines
166
+
167
+ # Find the end of the context section
168
+ end_index = find_section_end(lines, context_line_index, 2)
169
+
170
+ # Build the new content
171
+ new_lines = []
172
+ new_lines.concat(lines[0...context_line_index])
173
+ new_lines << "## Context\n"
174
+ new_lines << "\n"
175
+ new_lines.concat(context_content.lines)
176
+ new_lines.concat(lines[end_index..-1])
177
+
178
+ new_lines.join
179
+ end
180
+
181
+ def insert_context_section_after_agent(content, agent_line_index, context_content)
182
+ lines = content.lines
183
+
184
+ # Build the new content
185
+ new_lines = []
186
+ new_lines.concat(lines[0..agent_line_index])
187
+ new_lines << "\n"
188
+ new_lines << "## Context\n"
189
+ new_lines << "\n"
190
+ new_lines.concat(context_content.lines)
191
+ new_lines.concat(lines[agent_line_index + 1..-1])
192
+
193
+ new_lines.join
194
+ end
195
+
196
+ def prepend_agent_with_context(content, context_content)
197
+ agent_context = [
198
+ "# Agent",
199
+ "",
200
+ "## Context",
201
+ "",
202
+ context_content,
203
+ ""
204
+ ].join("\n")
205
+ agent_context + content
206
+ end
207
+
208
+ def find_section_end(lines, start_index, heading_level)
209
+ index = start_index + 1
210
+
211
+ while index < lines.length
212
+ line = lines[index]
213
+
214
+ if line.strip.start_with?("#")
215
+ level = line.strip.match(/^(#+)/)[1].length
216
+ if level <= heading_level
217
+ break
218
+ end
219
+ end
220
+
221
+ index += 1
222
+ end
223
+
224
+ index
225
+ end
226
+
227
+ def collect_gem_contexts
228
+ gem_contexts = {}
229
+
230
+ return gem_contexts unless Dir.exist?(@context_path)
231
+
232
+ Dir.glob(File.join(@context_path, "*")).each do |gem_directory|
233
+ next unless File.directory?(gem_directory)
234
+ gem_name = File.basename(gem_directory)
235
+
236
+ markdown_files = Dir.glob(File.join(gem_directory, "**", "*.md")).sort
237
+ gem_contexts[gem_name] = markdown_files if markdown_files.any?
238
+ end
239
+
240
+ gem_contexts
241
+ end
242
+
243
+ # Load a gem's index file
244
+ def load_gem_index(gem_name, gem_directory)
245
+ index_path = File.join(gem_directory, "index.yaml")
246
+
247
+ if File.exist?(index_path)
248
+ YAML.load_file(index_path)
249
+ else
250
+ # Return a fallback index if no index.yaml exists
251
+ {
252
+ "description" => "Context files for #{gem_name}",
253
+ "files" => []
254
+ }
255
+ end
256
+ rescue => error
257
+ Console.debug("Error loading index for #{gem_name}: #{error.message}")
258
+ # Return a fallback index
259
+ {
260
+ "description" => "Context files for #{gem_name}",
261
+ "files" => []
262
+ }
263
+ end
264
+
265
+ def extract_content(file_path)
266
+ content = File.read(file_path)
267
+ lines = content.lines.map(&:strip)
268
+
269
+ title = extract_title(lines)
270
+ description = extract_description(lines)
271
+
272
+ [title, description]
273
+ end
274
+
275
+ def extract_title(lines)
276
+ # Look for the first markdown header
277
+ header_line = lines.find { |line| line.start_with?("#") }
278
+ if header_line
279
+ # Remove markdown header syntax and clean up
280
+ header_line.sub(/^#+\s*/, "").strip
281
+ else
282
+ # If no header found, use a default
283
+ "Documentation"
284
+ end
285
+ end
286
+
287
+ def extract_description(lines)
288
+ # Skip empty lines and headers to find the first paragraph
289
+ content_start = false
290
+ description_lines = []
291
+
292
+ lines.each do |line|
293
+ # Skip headers
294
+ next if line.start_with?("#")
295
+
296
+ # Skip empty lines until we find content
297
+ if !content_start && line.empty?
298
+ next
299
+ end
300
+
301
+ # Mark that we've found content
302
+ content_start = true
303
+
304
+ # If we hit an empty line after finding content, we've reached the end of the first paragraph
305
+ if line.empty?
306
+ break
307
+ end
308
+
309
+ description_lines << line
310
+ end
311
+
312
+ # Join the lines and truncate if too long
313
+ description = description_lines.join(" ").strip
314
+ if description.length > 197
315
+ description = description[0..196] + "..."
316
+ end
317
+
318
+ description
319
+ end
320
+ end
321
+ end
322
+ end