ruboty-ai_agent 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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +45 -0
  5. data/AGENTS.md +22 -0
  6. data/CHANGELOG.md +3 -0
  7. data/CLAUDE.md +1 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +14 -0
  10. data/README.md +118 -0
  11. data/Rakefile +47 -0
  12. data/Steepfile +12 -0
  13. data/bin/console +15 -0
  14. data/bin/ruboty +34 -0
  15. data/bin/setup +8 -0
  16. data/lib/ruboty/ai_agent/actions/add_ai_command.rb +22 -0
  17. data/lib/ruboty/ai_agent/actions/add_ai_memory.rb +20 -0
  18. data/lib/ruboty/ai_agent/actions/add_mcp.rb +94 -0
  19. data/lib/ruboty/ai_agent/actions/base.rb +43 -0
  20. data/lib/ruboty/ai_agent/actions/chat.rb +64 -0
  21. data/lib/ruboty/ai_agent/actions/list_ai_commands.rb +19 -0
  22. data/lib/ruboty/ai_agent/actions/list_ai_memories.rb +18 -0
  23. data/lib/ruboty/ai_agent/actions/list_mcp.rb +18 -0
  24. data/lib/ruboty/ai_agent/actions/remove_ai_command.rb +18 -0
  25. data/lib/ruboty/ai_agent/actions/remove_ai_memory.rb +25 -0
  26. data/lib/ruboty/ai_agent/actions/remove_mcp.rb +24 -0
  27. data/lib/ruboty/ai_agent/actions/set_system_prompt.rb +31 -0
  28. data/lib/ruboty/ai_agent/actions/show_system_prompt.rb +30 -0
  29. data/lib/ruboty/ai_agent/actions.rb +22 -0
  30. data/lib/ruboty/ai_agent/agent.rb +71 -0
  31. data/lib/ruboty/ai_agent/cached_value.rb +43 -0
  32. data/lib/ruboty/ai_agent/chat_message.rb +60 -0
  33. data/lib/ruboty/ai_agent/chat_thread.rb +31 -0
  34. data/lib/ruboty/ai_agent/chat_thread_associations.rb +34 -0
  35. data/lib/ruboty/ai_agent/chat_thread_messages.rb +17 -0
  36. data/lib/ruboty/ai_agent/commands/base.rb +39 -0
  37. data/lib/ruboty/ai_agent/commands/clear.rb +29 -0
  38. data/lib/ruboty/ai_agent/commands/compact.rb +80 -0
  39. data/lib/ruboty/ai_agent/commands/usage.rb +52 -0
  40. data/lib/ruboty/ai_agent/commands.rb +33 -0
  41. data/lib/ruboty/ai_agent/database/query_methods.rb +84 -0
  42. data/lib/ruboty/ai_agent/database.rb +40 -0
  43. data/lib/ruboty/ai_agent/global_settings.rb +33 -0
  44. data/lib/ruboty/ai_agent/http_mcp_client.rb +215 -0
  45. data/lib/ruboty/ai_agent/llm/openai/model.rb +29 -0
  46. data/lib/ruboty/ai_agent/llm/openai.rb +181 -0
  47. data/lib/ruboty/ai_agent/llm/response.rb +21 -0
  48. data/lib/ruboty/ai_agent/llm.rb +11 -0
  49. data/lib/ruboty/ai_agent/mcp_clients.rb +48 -0
  50. data/lib/ruboty/ai_agent/mcp_configuration.rb +31 -0
  51. data/lib/ruboty/ai_agent/record_set.rb +71 -0
  52. data/lib/ruboty/ai_agent/recordable.rb +116 -0
  53. data/lib/ruboty/ai_agent/token_usage.rb +45 -0
  54. data/lib/ruboty/ai_agent/tool.rb +29 -0
  55. data/lib/ruboty/ai_agent/user.rb +52 -0
  56. data/lib/ruboty/ai_agent/user_ai_memories.rb +17 -0
  57. data/lib/ruboty/ai_agent/user_associations.rb +34 -0
  58. data/lib/ruboty/ai_agent/user_mcp_caches.rb +90 -0
  59. data/lib/ruboty/ai_agent/user_mcp_client.rb +93 -0
  60. data/lib/ruboty/ai_agent/user_mcp_configurations.rb +15 -0
  61. data/lib/ruboty/ai_agent/user_mcp_tools_caches.rb +14 -0
  62. data/lib/ruboty/ai_agent/version.rb +7 -0
  63. data/lib/ruboty/ai_agent.rb +40 -0
  64. data/lib/ruboty/handlers/ai_agent.rb +84 -0
  65. data/rbs_collection.yaml +23 -0
  66. data/ruboty-ai_agent.gemspec +49 -0
  67. data/script/generate-concern-rbs.rb +351 -0
  68. data/script/generate-data-rbs.rb +250 -0
  69. data/script/generate-memorized-ivar-rbs.rb +292 -0
  70. data/sig/generated/ruboty/ai_agent/actions/add_ai_command.rbs +16 -0
  71. data/sig/generated/ruboty/ai_agent/actions/add_ai_memory.rbs +14 -0
  72. data/sig/generated/ruboty/ai_agent/actions/add_mcp.rbs +26 -0
  73. data/sig/generated/ruboty/ai_agent/actions/base.rbs +34 -0
  74. data/sig/generated/ruboty/ai_agent/actions/chat.rbs +17 -0
  75. data/sig/generated/ruboty/ai_agent/actions/list_ai_commands.rbs +13 -0
  76. data/sig/generated/ruboty/ai_agent/actions/list_ai_memories.rbs +12 -0
  77. data/sig/generated/ruboty/ai_agent/actions/list_mcp.rbs +12 -0
  78. data/sig/generated/ruboty/ai_agent/actions/remove_ai_command.rbs +14 -0
  79. data/sig/generated/ruboty/ai_agent/actions/remove_ai_memory.rbs +14 -0
  80. data/sig/generated/ruboty/ai_agent/actions/remove_mcp.rbs +14 -0
  81. data/sig/generated/ruboty/ai_agent/actions/set_system_prompt.rbs +16 -0
  82. data/sig/generated/ruboty/ai_agent/actions/show_system_prompt.rbs +12 -0
  83. data/sig/generated/ruboty/ai_agent/actions.rbs +9 -0
  84. data/sig/generated/ruboty/ai_agent/agent.rbs +29 -0
  85. data/sig/generated/ruboty/ai_agent/cached_value.rbs +28 -0
  86. data/sig/generated/ruboty/ai_agent/chat_message.rbs +34 -0
  87. data/sig/generated/ruboty/ai_agent/chat_thread.rbs +22 -0
  88. data/sig/generated/ruboty/ai_agent/chat_thread_associations.rbs +21 -0
  89. data/sig/generated/ruboty/ai_agent/chat_thread_messages.rbs +13 -0
  90. data/sig/generated/ruboty/ai_agent/commands/base.rbs +40 -0
  91. data/sig/generated/ruboty/ai_agent/commands/clear.rbs +20 -0
  92. data/sig/generated/ruboty/ai_agent/commands/compact.rbs +30 -0
  93. data/sig/generated/ruboty/ai_agent/commands/usage.rbs +26 -0
  94. data/sig/generated/ruboty/ai_agent/commands.rbs +13 -0
  95. data/sig/generated/ruboty/ai_agent/database/query_methods.rbs +39 -0
  96. data/sig/generated/ruboty/ai_agent/database.rbs +27 -0
  97. data/sig/generated/ruboty/ai_agent/global_settings.rbs +23 -0
  98. data/sig/generated/ruboty/ai_agent/http_mcp_client.rbs +62 -0
  99. data/sig/generated/ruboty/ai_agent/llm/openai/model.rbs +21 -0
  100. data/sig/generated/ruboty/ai_agent/llm/openai.rbs +54 -0
  101. data/sig/generated/ruboty/ai_agent/llm/response.rbs +29 -0
  102. data/sig/generated/ruboty/ai_agent/llm.rbs +9 -0
  103. data/sig/generated/ruboty/ai_agent/mcp_clients.rbs +24 -0
  104. data/sig/generated/ruboty/ai_agent/mcp_configuration.rbs +35 -0
  105. data/sig/generated/ruboty/ai_agent/record_set.rbs +42 -0
  106. data/sig/generated/ruboty/ai_agent/recordable.rbs +56 -0
  107. data/sig/generated/ruboty/ai_agent/token_usage.rbs +30 -0
  108. data/sig/generated/ruboty/ai_agent/tool.rbs +27 -0
  109. data/sig/generated/ruboty/ai_agent/user.rbs +35 -0
  110. data/sig/generated/ruboty/ai_agent/user_ai_memories.rbs +11 -0
  111. data/sig/generated/ruboty/ai_agent/user_associations.rbs +21 -0
  112. data/sig/generated/ruboty/ai_agent/user_mcp_caches.rbs +44 -0
  113. data/sig/generated/ruboty/ai_agent/user_mcp_client.rbs +58 -0
  114. data/sig/generated/ruboty/ai_agent/user_mcp_configurations.rbs +11 -0
  115. data/sig/generated/ruboty/ai_agent/user_mcp_tools_caches.rbs +11 -0
  116. data/sig/generated/ruboty/ai_agent/version.rbs +7 -0
  117. data/sig/generated/ruboty/ai_agent.rbs +9 -0
  118. data/sig/generated/ruboty/handlers/ai_agent.rbs +32 -0
  119. data/sig/generated-by-scripts/concerns.rbs +27 -0
  120. data/sig/generated-by-scripts/memorized_ivars.rbs +42 -0
  121. data/sig-lib/event_stream_parser/event_stream_parser.rbs +21 -0
  122. data/sig-lib/mem/mem.rbs +19 -0
  123. data/sig-lib/ruboty/ruboty.rbs +421 -0
  124. metadata +263 -0
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruboty'
4
+
5
+ module Ruboty
6
+ module Handlers
7
+ # Handler for AI agent commands
8
+ class AiAgent < Base
9
+ env :OPENAI_API_KEY, 'Pass your OpenAI API Key'
10
+ env :OPENAI_MODEL, 'OpenAI model to use', optional: true
11
+
12
+ on(
13
+ /(?<body>.+)/m,
14
+ description: 'AI responds to your message if given message did not match any other handlers',
15
+ missing: true,
16
+ name: 'chat'
17
+ )
18
+
19
+ on(/add mcp (?<name>\S+)\s+(?<config>.+)\z/, name: 'add_mcp', description: 'Add a new MCP server')
20
+ on(/remove mcp (?<name>\S+)/, name: 'remove_mcp', description: 'Remove the specified MCP server')
21
+ on(/list mcps?/, name: 'list_mcp', description: 'List configured MCP servers')
22
+
23
+ on(/set system prompt "(?<prompt>.+?)"(?: in (?<scope>user|global) scope)?/, name: 'set_system_prompt', description: 'Set system prompt')
24
+ on(/show system prompt/, name: 'show_system_prompt', description: 'Show system prompt')
25
+
26
+ on(/add ai memory "(?<prompt>.+)"/, name: 'add_ai_memory', description: 'Add a new AI memory')
27
+ on(/remove ai memory (?<index>\d+)/, name: 'remove_ai_memory', description: 'Remove the specified AI memory')
28
+ on(/list ai memor(?:y|ies)/, name: 'list_mcp', description: "List AI's memories")
29
+
30
+ on(%r{add ai command /(?<name>\S+)\s+"(?<prompt>.+)"}, name: 'add_ai_command',
31
+ description: 'Add a new AI command')
32
+ on(%r{remove ai command /(?<name>\S+)}, name: 'remove_ai_command', description: 'Remove the specified AI command')
33
+ on(/list ai commands?/, name: 'list_ai_commands', description: 'List AI commands')
34
+
35
+ def chat(message)
36
+ Ruboty::AiAgent::Actions::Chat.call(message)
37
+ end
38
+
39
+ def add_mcp(message)
40
+ Ruboty::AiAgent::Actions::AddMcp.call(message)
41
+ end
42
+
43
+ def remove_mcp(message)
44
+ Ruboty::AiAgent::Actions::RemoveMcp.call(message)
45
+ end
46
+
47
+ def list_mcp(message)
48
+ Ruboty::AiAgent::Actions::ListMcp.call(message)
49
+ end
50
+
51
+ def set_system_prompt(message)
52
+ Ruboty::AiAgent::Actions::SetSystemPrompt.call(message)
53
+ end
54
+
55
+ def show_system_prompt(message)
56
+ Ruboty::AiAgent::Actions::ShowSystemPrompt.call(message)
57
+ end
58
+
59
+ def add_ai_memory(message)
60
+ Ruboty::AiAgent::Actions::AddAiMemory.call(message)
61
+ end
62
+
63
+ def remove_ai_memory(message)
64
+ Ruboty::AiAgent::Actions::RemoveAiMemory.call(message)
65
+ end
66
+
67
+ def list_ai_memories(message)
68
+ Ruboty::AiAgent::Actions::ListAiMemories.call(message)
69
+ end
70
+
71
+ def add_ai_command(message)
72
+ Ruboty::AiAgent::Actions::AddAiCommand.call(message)
73
+ end
74
+
75
+ def remove_ai_command(message)
76
+ Ruboty::AiAgent::Actions::RemoveAiCommand.call(message)
77
+ end
78
+
79
+ def list_ai_commands(message)
80
+ Ruboty::AiAgent::Actions::ListAiCommands.call(message)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ # Download sources
2
+ sources:
3
+ - type: git
4
+ name: ruby/gem_rbs_collection
5
+ remote: https://github.com/ruby/gem_rbs_collection.git
6
+ revision: main
7
+ repo_dir: gems
8
+
9
+ # You can specify local directories as sources also.
10
+ # - type: local
11
+ # path: path/to/your/local/repository
12
+
13
+ # A directory to install the downloaded RBSs
14
+ path: .gem_rbs_collection
15
+
16
+ gems:
17
+ # stdlibs
18
+ - name: net-http
19
+ - name: json
20
+ - name: optparse
21
+ # # If you want to avoid installing rbs files for gems, you can specify them here.
22
+ # - name: GEM_NAME
23
+ # ignore: true
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'ruboty/ai_agent/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'ruboty-ai_agent'
9
+ spec.version = Ruboty::AiAgent::VERSION
10
+ spec.authors = ['Tomoya Chiba']
11
+ spec.email = ['tomo.asleep@gmail.com']
12
+
13
+ spec.summary = 'A Ruboty plugin that uses AI to generate responses.'
14
+ spec.description = 'A Ruboty plugin that uses AI to generate responses.'
15
+ spec.homepage = 'https://github.com/tomoasleep/ruboty-ai_agent'
16
+
17
+ spec.required_ruby_version = '>= 3.1.0'
18
+
19
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
20
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
21
+ if spec.respond_to?(:metadata)
22
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
23
+
24
+ spec.metadata['homepage_uri'] = spec.homepage
25
+ spec.metadata['source_code_uri'] = 'https://github.com/tomoasleep/ruboty-ai_agent'
26
+ spec.metadata['changelog_uri'] = 'https://github.com/tomoasleep/ruboty-ai_agent/blob/main/CHANGELOG.md'
27
+ else
28
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
29
+ 'public gem pushes.'
30
+ end
31
+
32
+ # Specify which files should be added to the gem when it is released.
33
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
34
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
35
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
36
+ end
37
+ spec.bindir = 'exe'
38
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
39
+ spec.require_paths = ['lib']
40
+
41
+ spec.add_development_dependency 'bundler', '~> 2'
42
+ spec.add_development_dependency 'rake', '~> 13'
43
+ spec.add_development_dependency 'rspec', '~> 3.0'
44
+ spec.add_development_dependency 'webmock', '~> 3.0'
45
+
46
+ spec.add_dependency 'event_stream_parser', '~> 1.0'
47
+ spec.add_dependency 'openai', '~> 0.22.0'
48
+ spec.add_dependency 'ruboty', '~> 1.3'
49
+ end
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Generate RBS definitions for classes/modules that include concern modules
5
+ #
6
+ # This script detects concern modules (modules with ClassMethods or PrependMethods)
7
+ # and generates extend/prepend declarations for classes that include them.
8
+ #
9
+ # Example Ruby code:
10
+ #
11
+ # module Trackable
12
+ # def self.included(base)
13
+ # base.extend(ClassMethods)
14
+ # base.prepend(PrependMethods)
15
+ # end
16
+ #
17
+ # module ClassMethods
18
+ # def track_method(name)
19
+ # # ...
20
+ # end
21
+ # end
22
+ #
23
+ # module PrependMethods
24
+ # def initialize(*)
25
+ # super
26
+ # # tracking logic
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # class User
32
+ # include Trackable
33
+ # end
34
+ #
35
+ # Existing RBS:
36
+ #
37
+ # module Trackable
38
+ # module ClassMethods
39
+ # def track_method: (Symbol) -> void
40
+ # end
41
+ #
42
+ # module PrependMethods
43
+ # def initialize: (*untyped) -> void
44
+ # end
45
+ # end
46
+ #
47
+ # class User
48
+ # include Trackable
49
+ # end
50
+ #
51
+ # This script will generate:
52
+ #
53
+ # class User
54
+ # extend Trackable::ClassMethods
55
+ # prepend Trackable::PrependMethods
56
+ # end
57
+ #
58
+ # The script:
59
+ # 1. Loads existing RBS definitions from sig/ directory
60
+ # 2. Finds modules that have ClassMethods or PrependMethods submodules
61
+ # 3. Finds classes/modules that include these concern modules
62
+ # 4. Generates appropriate extend/prepend declarations
63
+
64
+ require 'rbs'
65
+ require 'rbs/environment'
66
+ require 'pathname'
67
+ require 'fileutils'
68
+
69
+ # Process Ruby source files and RBS definitions to generate concern RBS
70
+ class ConcernRbsGenerator
71
+ def initialize(lib_path: 'lib', sig_path: 'sig', output_path: 'sig/generated-by-scripts', namespace_filter: nil)
72
+ @lib_path = Pathname(lib_path)
73
+ @sig_path = Pathname(sig_path)
74
+ @output_path = Pathname(output_path)
75
+ @namespace_filter = namespace_filter
76
+ @env = RBS::Environment.new
77
+ end
78
+
79
+ def process
80
+ load_rbs_environment
81
+ generate_concern_rbs
82
+ end
83
+
84
+ private
85
+
86
+ def load_rbs_environment
87
+ puts 'Loading RBS environment...'
88
+
89
+ # Load standard library RBS
90
+ loader = RBS::EnvironmentLoader.new
91
+
92
+ # Add sig subdirectories, excluding generated-by-scripts
93
+ Pathname('sig').children.select(&:directory?).each do |dir|
94
+ next if dir.basename.to_s == 'generated-by-scripts'
95
+
96
+ loader.add(path: dir)
97
+ end
98
+
99
+ loader.add(path: Pathname('.gem_rbs_collection'))
100
+ loader.load(env: @env)
101
+
102
+ puts "Loaded #{@env.class_decls.size} class declarations"
103
+ puts "Loaded #{@env.interface_decls.size} interface declarations"
104
+ end
105
+
106
+ def generate_concern_rbs
107
+ puts "\nGenerating concern RBS..."
108
+
109
+ concern_modules = find_concern_modules
110
+ includers = find_concern_includers(concern_modules)
111
+
112
+ if includers.empty?
113
+ puts 'No classes/modules found that include concern modules'
114
+ return
115
+ end
116
+
117
+ puts "\nFound #{includers.size} classes/modules that include concerns:"
118
+ includers.each do |includer|
119
+ puts " - #{includer[:name]} includes #{includer[:concern][:name]}"
120
+ end
121
+
122
+ rbs_content = generate_rbs_content(includers)
123
+
124
+ return unless rbs_content
125
+
126
+ FileUtils.mkdir_p(@output_path)
127
+ output_file = @output_path / 'concerns.rbs'
128
+ File.write(output_file, rbs_content)
129
+ puts "\nGenerated #{output_file}"
130
+ end
131
+
132
+ def find_concern_modules
133
+ concern_modules = []
134
+
135
+ @env.class_decls.each do |type_name, decl|
136
+ # Skip if not a module
137
+ next unless decl.primary.decl.is_a?(RBS::AST::Declarations::Module)
138
+
139
+ module_name = type_name.to_s
140
+
141
+ # Filter by namespace if specified
142
+ next if @namespace_filter && !module_name.start_with?("::#{@namespace_filter}")
143
+
144
+ # Check if module has a ClassMethods submodule
145
+ class_methods_name = RBS::TypeName.new(
146
+ namespace: RBS::Namespace.new(path: type_name.namespace.path + [type_name.name], absolute: type_name.namespace.absolute?),
147
+ name: :ClassMethods
148
+ )
149
+
150
+ # Check if module has a PrependMethods submodule
151
+ prepend_methods_name = RBS::TypeName.new(
152
+ namespace: RBS::Namespace.new(path: type_name.namespace.path + [type_name.name], absolute: type_name.namespace.absolute?),
153
+ name: :PrependMethods
154
+ )
155
+
156
+ has_class_methods = @env.class_decls.key?(class_methods_name)
157
+ has_prepend_methods = @env.class_decls.key?(prepend_methods_name)
158
+
159
+ next unless has_class_methods || has_prepend_methods
160
+
161
+ concern_modules << {
162
+ name: module_name,
163
+ type_name: type_name,
164
+ has_class_methods: has_class_methods,
165
+ has_prepend_methods: has_prepend_methods
166
+ }
167
+ end
168
+
169
+ concern_modules
170
+ end
171
+
172
+ def find_concern_includers(concern_modules)
173
+ includers = []
174
+
175
+ @env.class_decls.each do |type_name, decl|
176
+ # Filter by namespace if specified
177
+ next if @namespace_filter && !type_name.to_s.start_with?("::#{@namespace_filter}")
178
+
179
+ # Check all declarations (primary and others) for includes
180
+ all_decls = [decl.primary] + decl.decls
181
+
182
+ includes = []
183
+ is_class = false
184
+
185
+ all_decls.each do |d|
186
+ case d.decl
187
+ when RBS::AST::Declarations::Class
188
+ is_class = true
189
+ d.decl.members.each do |member|
190
+ case member
191
+ when RBS::AST::Members::Include
192
+ # Resolve the included module name to full TypeName
193
+ included_type_name = resolve_type_name(member.name, type_name.namespace)
194
+ includes << included_type_name if included_type_name
195
+ end
196
+ end
197
+ when RBS::AST::Declarations::Module
198
+ d.decl.members.each do |member|
199
+ case member
200
+ when RBS::AST::Members::Include
201
+ # Resolve the included module name to full TypeName
202
+ included_type_name = resolve_type_name(member.name, type_name.namespace)
203
+ includes << included_type_name if included_type_name
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ # Check if any of the includes are concern modules
210
+ includes.uniq.each do |included_name|
211
+ concern = concern_modules.find do |c|
212
+ c[:type_name] == included_name
213
+ end
214
+
215
+ next unless concern
216
+
217
+ # Get type parameters from Ruby source file
218
+ type_params = extract_type_parameters_from_source(type_name)
219
+
220
+ includers << {
221
+ name: type_name.to_s,
222
+ type_name: type_name,
223
+ is_class: is_class,
224
+ concern: concern,
225
+ type_params: type_params
226
+ }
227
+ end
228
+ end
229
+
230
+ includers
231
+ end
232
+
233
+ def extract_type_parameters_from_source(type_name)
234
+ # Convert type name to file path
235
+ # e.g., ::Ruboty::AiAgent::CachedValue -> lib/ruboty/ai_agent/cached_value.rb
236
+ path_parts = type_name.to_s.sub(/^::/, '').split('::').map do |part|
237
+ part.gsub(/([A-Z]+)([A-Z][a-z])|([a-z\d])([A-Z])/) do
238
+ "#{Regexp.last_match(1) || Regexp.last_match(3)}_#{Regexp.last_match(2) || Regexp.last_match(4)}"
239
+ end.downcase
240
+ end
241
+
242
+ file_path = @lib_path / "#{path_parts.join('/')}.rb"
243
+ return nil unless file_path.exist?
244
+
245
+ content = file_path.read
246
+ lines = content.lines
247
+
248
+ # Find the class/module definition line
249
+ class_or_module_name = type_name.to_s.split('::').last
250
+
251
+ lines.each_with_index do |line, index|
252
+ # Look for @rbs generic comment before class/module definition
253
+ next unless line =~ /^\s*#\s*@rbs\s+generic\s+(.+)$/
254
+
255
+ type_params_str = Regexp.last_match(1).strip
256
+ # Check if next non-comment line is the class/module definition
257
+ next_line_index = index + 1
258
+ while next_line_index < lines.length
259
+ next_line = lines[next_line_index]
260
+ # Skip comment lines and empty lines
261
+ break unless next_line =~ /^\s*(#|$)/
262
+
263
+ next_line_index += 1
264
+ end
265
+
266
+ next unless next_line_index < lines.length
267
+
268
+ next_line = lines[next_line_index]
269
+ if next_line =~ /^\s*(class|module)\s+#{Regexp.escape(class_or_module_name)}\b/
270
+ # Parse type parameters (e.g., "D" or "K, V")
271
+ return type_params_str.split(',').map(&:strip)
272
+ end
273
+ end
274
+
275
+ nil
276
+ end
277
+
278
+ def resolve_type_name(name, current_namespace)
279
+ # If the name is already absolute, use it as is
280
+ return name if name.absolute?
281
+
282
+ # Otherwise, resolve relative to current namespace
283
+ RBS::TypeName.new(
284
+ namespace: current_namespace,
285
+ name: name.name
286
+ )
287
+ end
288
+
289
+ def generate_rbs_content(includers)
290
+ content = []
291
+ content << '# Generated RBS definitions for concern modules'
292
+ content << '# This file is automatically generated by script/generate-concern-rbs.rb'
293
+ content << ''
294
+
295
+ # Group by namespace for cleaner output
296
+ grouped = includers.group_by do |includer|
297
+ parts = includer[:name].sub(/^::/, '').split('::')
298
+ parts[0...-1]
299
+ end
300
+
301
+ grouped.each do |namespace_parts, group_includers|
302
+ indent_level = 0
303
+
304
+ # Open namespaces
305
+ namespace_parts.each do |part|
306
+ content << (' ' * indent_level) + "module #{part}"
307
+ indent_level += 1
308
+ end
309
+
310
+ # Add extend/prepend for each includer in this namespace
311
+ group_includers.each do |includer|
312
+ simple_name = includer[:name].sub(/^::/, '').split('::').last
313
+ keyword = includer[:is_class] ? 'class' : 'module'
314
+
315
+ # Add type parameters if present
316
+ if includer[:type_params] && !includer[:type_params].empty?
317
+ type_params_str = "[#{includer[:type_params].join(', ')}]"
318
+ content << (' ' * indent_level) + "#{keyword} #{simple_name}#{type_params_str}"
319
+ else
320
+ content << (' ' * indent_level) + "#{keyword} #{simple_name}"
321
+ end
322
+
323
+ concern = includer[:concern]
324
+ concern_simple_name = concern[:name].sub(/^::/, '').split('::').last
325
+
326
+ content << (' ' * (indent_level + 1)) + "extend #{concern_simple_name}::ClassMethods" if concern[:has_class_methods]
327
+
328
+ content << (' ' * (indent_level + 1)) + "prepend #{concern_simple_name}::PrependMethods" if concern[:has_prepend_methods]
329
+
330
+ content << "#{' ' * indent_level}end"
331
+ content << ''
332
+ end
333
+
334
+ # Close namespaces
335
+ namespace_parts.size.times do
336
+ indent_level -= 1
337
+ content << "#{' ' * indent_level}end"
338
+ end
339
+
340
+ content << ''
341
+ end
342
+
343
+ content.join("\n").strip
344
+ end
345
+ end
346
+
347
+ # Run if executed directly
348
+ if __FILE__ == $PROGRAM_NAME
349
+ generator = ConcernRbsGenerator.new(namespace_filter: 'Ruboty')
350
+ generator.process
351
+ end