agent-context 0.0.2 → 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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/agent.md +37 -0
- data/bake/agent/context.rb +22 -10
- data/context/usage.md +173 -0
- data/lib/agent/context/index.rb +322 -0
- data/lib/agent/context/installer.rb +270 -0
- data/lib/agent/context/version.rb +1 -1
- data/lib/agent/context.rb +9 -1
- data/post.md +149 -0
- data/readme.md +110 -25
- data/specification.md +176 -0
- data.tar.gz.sig +0 -0
- metadata +21 -6
- metadata.gz.sig +0 -0
- data/context/adding-context.md +0 -162
- data/context/examples.md +0 -87
- data/context/getting-started.md +0 -51
- data/design.md +0 -149
- data/lib/agent/context/helper.rb +0 -114
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71c3555039c6fe5ae6c08d31a01b7ad69fdbc37d5dc13ddedbd9066fbd42a202
|
4
|
+
data.tar.gz: 496c8d37bc994834ca49328976f53cedef923c1aafd76073eac1218e27322579
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/bake/agent/context.rb
CHANGED
@@ -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/
|
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
|
-
@
|
15
|
+
@installer = Installer.new(root: context.root)
|
15
16
|
end
|
16
17
|
|
17
|
-
attr :
|
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 = @
|
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(@
|
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 = @
|
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 = @
|
51
|
+
content = @installer.show_context_file(gem, file)
|
51
52
|
if content
|
52
53
|
puts content
|
53
54
|
else
|
@@ -59,13 +60,13 @@ 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 @
|
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 = @
|
69
|
+
installed = @installer.install_all_context
|
69
70
|
if installed.any?
|
70
71
|
puts "Installed context from #{installed.length} gems:"
|
71
72
|
installed.each { |gem_name| puts " #{gem_name}" }
|
@@ -73,4 +74,15 @@ def install(gem: nil)
|
|
73
74
|
puts "No gems with context found"
|
74
75
|
end
|
75
76
|
end
|
76
|
-
|
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
|