girb 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 +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +253 -0
- data/README_ja.md +251 -0
- data/Rakefile +8 -0
- data/exe/girb +38 -0
- data/lib/girb/ai_client.rb +157 -0
- data/lib/girb/configuration.rb +39 -0
- data/lib/girb/context_builder.rb +134 -0
- data/lib/girb/conversation_history.rb +132 -0
- data/lib/girb/exception_capture.rb +54 -0
- data/lib/girb/irb_integration.rb +87 -0
- data/lib/girb/prompt_builder.rb +173 -0
- data/lib/girb/providers/base.rb +65 -0
- data/lib/girb/session_history.rb +197 -0
- data/lib/girb/tools/base.rb +61 -0
- data/lib/girb/tools/evaluate_code.rb +43 -0
- data/lib/girb/tools/find_file.rb +93 -0
- data/lib/girb/tools/get_source.rb +145 -0
- data/lib/girb/tools/inspect_object.rb +54 -0
- data/lib/girb/tools/list_methods.rb +66 -0
- data/lib/girb/tools/rails_tools.rb +141 -0
- data/lib/girb/tools/read_file.rb +124 -0
- data/lib/girb/tools/session_history_tool.rb +183 -0
- data/lib/girb/tools.rb +38 -0
- data/lib/girb/version.rb +5 -0
- data/lib/girb.rb +53 -0
- data/lib/irb/command/qq.rb +55 -0
- metadata +131 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
class PromptBuilder
|
|
5
|
+
SYSTEM_PROMPT = <<~PROMPT
|
|
6
|
+
You are girb, an AI assistant embedded in a Ruby developer's IRB session.
|
|
7
|
+
|
|
8
|
+
## CRITICAL: Prompt Information Takes Highest Priority
|
|
9
|
+
Information in this system prompt and "User-Defined Instructions" section
|
|
10
|
+
takes precedence over tool results or user input.
|
|
11
|
+
When asked about environment or preconditions, check this prompt first.
|
|
12
|
+
Always verify if the information is already stated here before attempting programmatic detection.
|
|
13
|
+
|
|
14
|
+
## Language
|
|
15
|
+
Respond in the same language the user is using. Detect the user's language from their question and match it.
|
|
16
|
+
|
|
17
|
+
## Important: Understand the IRB Session Context
|
|
18
|
+
The user is interactively executing code in IRB and asking questions within that flow.
|
|
19
|
+
"Session History" contains the code the user has executed and past AI conversations in chronological order.
|
|
20
|
+
Always interpret questions in the context of this history.
|
|
21
|
+
|
|
22
|
+
For example, if the history shows:
|
|
23
|
+
1: a = 1
|
|
24
|
+
2: b = 2
|
|
25
|
+
3: [USER] What will z be if I continue with c = 3 and beyond?
|
|
26
|
+
The user is asking about the value of z when continuing the pattern a=1, b=2, c=3... (answer: z=26).
|
|
27
|
+
|
|
28
|
+
## Your Role
|
|
29
|
+
- Strive to understand the user's true intent and background
|
|
30
|
+
- Don't just answer the question; understand what they're trying to achieve and what challenges they face
|
|
31
|
+
- Analyze session history to understand what the user is trying to do
|
|
32
|
+
- Utilize the current execution context (variables, object state, exceptions)
|
|
33
|
+
- Provide specific, practical answers to questions
|
|
34
|
+
- Use tools to execute and verify code as needed
|
|
35
|
+
|
|
36
|
+
## You May Ask Clarifying Questions
|
|
37
|
+
When you have doubts, ask the user about preconditions or unclear points.
|
|
38
|
+
- When multiple interpretations are possible: Confirm which interpretation is correct
|
|
39
|
+
- When preconditions are unclear: Ask what they're aiming for, what environment they're assuming
|
|
40
|
+
- When information is insufficient: Prompt for the full error message or related code
|
|
41
|
+
Asking questions increases dialogue turns but reduces misunderstandings and enables more accurate answers.
|
|
42
|
+
|
|
43
|
+
## Response Guidelines
|
|
44
|
+
- Keep responses concise and practical
|
|
45
|
+
- Read patterns and intentions; handle hypothetical questions
|
|
46
|
+
- Code examples should use variables and objects from the current IRB context and be directly executable by pasting into IRB
|
|
47
|
+
|
|
48
|
+
## Debugging Support on Errors
|
|
49
|
+
When users encounter errors, actively support debugging.
|
|
50
|
+
- Don't just point out the cause; show debugging steps to resolve it
|
|
51
|
+
- Suggest ways to inspect related code (e.g., using the inspect_object tool)
|
|
52
|
+
- Guide them step-by-step toward writing more robust code
|
|
53
|
+
|
|
54
|
+
## Available Tools
|
|
55
|
+
Use tools to inspect variables in detail, retrieve source code, and execute code.
|
|
56
|
+
Actively use the evaluate_code tool especially for verifying hypotheses and calculations.
|
|
57
|
+
PROMPT
|
|
58
|
+
|
|
59
|
+
def initialize(question, context)
|
|
60
|
+
@question = question
|
|
61
|
+
@context = context
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Legacy single prompt format (for backward compatibility)
|
|
65
|
+
def build
|
|
66
|
+
<<~PROMPT
|
|
67
|
+
#{system_prompt}
|
|
68
|
+
|
|
69
|
+
#{build_context_section}
|
|
70
|
+
|
|
71
|
+
## Question
|
|
72
|
+
#{@question}
|
|
73
|
+
PROMPT
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# System prompt (shared across conversation)
|
|
77
|
+
def system_prompt
|
|
78
|
+
custom = Girb.configuration&.custom_prompt
|
|
79
|
+
if custom && !custom.empty?
|
|
80
|
+
"#{SYSTEM_PROMPT}\n\n## User-Defined Instructions\n#{custom}"
|
|
81
|
+
else
|
|
82
|
+
SYSTEM_PROMPT
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# User message (context + question)
|
|
87
|
+
def user_message
|
|
88
|
+
<<~MSG
|
|
89
|
+
## Current IRB Context
|
|
90
|
+
#{build_context_section}
|
|
91
|
+
|
|
92
|
+
## Question
|
|
93
|
+
#{@question}
|
|
94
|
+
MSG
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def build_context_section
|
|
100
|
+
<<~CONTEXT
|
|
101
|
+
### Session History (Previous IRB Inputs)
|
|
102
|
+
Below is the code the user has executed so far. The question is asked within this flow.
|
|
103
|
+
#{format_session_history}
|
|
104
|
+
|
|
105
|
+
### Current Local Variables
|
|
106
|
+
#{format_locals}
|
|
107
|
+
|
|
108
|
+
### Last Evaluation Result
|
|
109
|
+
#{@context[:last_value] || "(none)"}
|
|
110
|
+
|
|
111
|
+
### Last Exception
|
|
112
|
+
#{format_exception}
|
|
113
|
+
|
|
114
|
+
### Methods Defined in IRB
|
|
115
|
+
#{format_method_definitions}
|
|
116
|
+
CONTEXT
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def format_source_location
|
|
120
|
+
loc = @context[:source_location]
|
|
121
|
+
return "(unknown)" unless loc
|
|
122
|
+
|
|
123
|
+
"File: #{loc[:file]}\nLine: #{loc[:line]}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def format_locals
|
|
127
|
+
return "(none)" if @context[:local_variables].nil? || @context[:local_variables].empty?
|
|
128
|
+
|
|
129
|
+
@context[:local_variables].map do |name, value|
|
|
130
|
+
"- #{name}: #{value}"
|
|
131
|
+
end.join("\n")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def format_self_info
|
|
135
|
+
info = @context[:self_info]
|
|
136
|
+
return "(unknown)" unless info
|
|
137
|
+
|
|
138
|
+
lines = ["Class: #{info[:class]}"]
|
|
139
|
+
lines << "inspect: #{info[:inspect]}"
|
|
140
|
+
if info[:methods]&.any?
|
|
141
|
+
lines << "Defined methods: #{info[:methods].join(', ')}"
|
|
142
|
+
end
|
|
143
|
+
lines.join("\n")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def format_exception
|
|
147
|
+
exc = @context[:last_exception]
|
|
148
|
+
return "(none)" unless exc
|
|
149
|
+
|
|
150
|
+
<<~EXC
|
|
151
|
+
Class: #{exc[:class]}
|
|
152
|
+
Message: #{exc[:message]}
|
|
153
|
+
Time: #{exc[:time]}
|
|
154
|
+
Backtrace:
|
|
155
|
+
#{exc[:backtrace]&.map { |l| " #{l}" }&.join("\n")}
|
|
156
|
+
EXC
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def format_session_history
|
|
160
|
+
history = @context[:session_history]
|
|
161
|
+
return "(none)" if history.nil? || history.empty?
|
|
162
|
+
|
|
163
|
+
history.join("\n")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def format_method_definitions
|
|
167
|
+
methods = @context[:method_definitions]
|
|
168
|
+
return "(none)" if methods.nil? || methods.empty?
|
|
169
|
+
|
|
170
|
+
methods.join("\n")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
module Providers
|
|
5
|
+
# Base class for LLM providers
|
|
6
|
+
# Implement this class to add support for new LLM providers
|
|
7
|
+
#
|
|
8
|
+
# Example:
|
|
9
|
+
# class MyProvider < Girb::Providers::Base
|
|
10
|
+
# def chat(messages:, system_prompt:, tools:)
|
|
11
|
+
# # Call your LLM API
|
|
12
|
+
# # Return Response object
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Girb.configure do |c|
|
|
17
|
+
# c.provider = MyProvider.new(api_key: "...")
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
class Base
|
|
21
|
+
# Send a chat request to the LLM
|
|
22
|
+
#
|
|
23
|
+
# @param messages [Array<Hash>] Conversation history in normalized format
|
|
24
|
+
# Each message has :role (:user, :assistant, :tool_call, :tool_result) and :content
|
|
25
|
+
# @param system_prompt [String] System prompt
|
|
26
|
+
# @param tools [Array<Hash>] Tool definitions in normalized format
|
|
27
|
+
# @return [Response] Response object with text and/or function_calls
|
|
28
|
+
def chat(messages:, system_prompt:, tools:)
|
|
29
|
+
raise NotImplementedError, "#{self.class}#chat must be implemented"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Response object returned by chat method
|
|
33
|
+
class Response
|
|
34
|
+
attr_reader :text, :function_calls, :error, :raw_response
|
|
35
|
+
|
|
36
|
+
def initialize(text: nil, function_calls: nil, error: nil, raw_response: nil)
|
|
37
|
+
@text = text
|
|
38
|
+
@function_calls = function_calls || []
|
|
39
|
+
@error = error
|
|
40
|
+
@raw_response = raw_response
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def function_call?
|
|
44
|
+
@function_calls.any?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Normalized tool definition format
|
|
49
|
+
# Providers should convert this to their specific format
|
|
50
|
+
#
|
|
51
|
+
# Example:
|
|
52
|
+
# {
|
|
53
|
+
# name: "evaluate_code",
|
|
54
|
+
# description: "Execute Ruby code",
|
|
55
|
+
# parameters: {
|
|
56
|
+
# type: "object",
|
|
57
|
+
# properties: {
|
|
58
|
+
# code: { type: "string", description: "Ruby code to execute" }
|
|
59
|
+
# },
|
|
60
|
+
# required: ["code"]
|
|
61
|
+
# }
|
|
62
|
+
# }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
# IRBセッション中の全入力を行番号付きで管理し、
|
|
5
|
+
# メソッド定義を追跡するクラス
|
|
6
|
+
class SessionHistory
|
|
7
|
+
Entry = Struct.new(:line_no, :code, :method_definition, :is_ai_question, :ai_response, :ai_reasoning, keyword_init: true)
|
|
8
|
+
MethodDef = Struct.new(:name, :start_line, :end_line, :code, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def instance
|
|
12
|
+
@instance ||= new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reset!
|
|
16
|
+
@instance = new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 委譲メソッド
|
|
20
|
+
def record(line_no, code, is_ai_question: false)
|
|
21
|
+
instance.record(line_no, code, is_ai_question: is_ai_question)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def entries
|
|
25
|
+
instance.entries
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def method_definitions
|
|
29
|
+
instance.method_definitions
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def find_by_line(line_no)
|
|
33
|
+
instance.find_by_line(line_no)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find_by_line_range(start_line, end_line)
|
|
37
|
+
instance.find_by_line_range(start_line, end_line)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def find_method(name)
|
|
41
|
+
instance.find_method(name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def all_with_line_numbers
|
|
45
|
+
instance.all_with_line_numbers
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def method_index
|
|
49
|
+
instance.method_index
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def record_ai_response(line_no, response, reasoning = nil)
|
|
53
|
+
instance.record_ai_response(line_no, response, reasoning)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def get_ai_detail(line_no)
|
|
57
|
+
instance.get_ai_detail(line_no)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ai_conversations
|
|
61
|
+
instance.ai_conversations
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
attr_reader :entries, :method_definitions
|
|
66
|
+
|
|
67
|
+
def initialize
|
|
68
|
+
@entries = []
|
|
69
|
+
@method_definitions = []
|
|
70
|
+
@pending_method = nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# IRBからの入力を記録
|
|
74
|
+
def record(line_no, code, is_ai_question: false)
|
|
75
|
+
code = code.to_s.chomp
|
|
76
|
+
|
|
77
|
+
entry = Entry.new(line_no: line_no, code: code, method_definition: nil, is_ai_question: is_ai_question)
|
|
78
|
+
|
|
79
|
+
# メソッド定義の開始を検出
|
|
80
|
+
if code.match?(/^\s*def\s+\w+/)
|
|
81
|
+
@pending_method = {
|
|
82
|
+
start_line: line_no,
|
|
83
|
+
code_lines: [code]
|
|
84
|
+
}
|
|
85
|
+
elsif @pending_method
|
|
86
|
+
@pending_method[:code_lines] << code
|
|
87
|
+
|
|
88
|
+
# メソッド定義の終了を検出(簡易的なend検出)
|
|
89
|
+
if code.strip == "end" || code.match?(/^\s*end\s*$/)
|
|
90
|
+
method_name = extract_method_name(@pending_method[:code_lines].first)
|
|
91
|
+
full_code = @pending_method[:code_lines].join("\n")
|
|
92
|
+
|
|
93
|
+
method_def = MethodDef.new(
|
|
94
|
+
name: method_name,
|
|
95
|
+
start_line: @pending_method[:start_line],
|
|
96
|
+
end_line: line_no,
|
|
97
|
+
code: full_code
|
|
98
|
+
)
|
|
99
|
+
@method_definitions << method_def
|
|
100
|
+
entry.method_definition = method_def
|
|
101
|
+
@pending_method = nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@entries << entry
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# 特定の行番号のエントリを取得
|
|
109
|
+
def find_by_line(line_no)
|
|
110
|
+
@entries.find { |e| e.line_no == line_no }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# 行範囲のエントリを取得
|
|
114
|
+
def find_by_line_range(start_line, end_line)
|
|
115
|
+
@entries.select { |e| e.line_no >= start_line && e.line_no <= end_line }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# メソッド名でメソッド定義を検索
|
|
119
|
+
def find_method(name)
|
|
120
|
+
name = name.to_s
|
|
121
|
+
@method_definitions.find { |m| m.name == name }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# 全履歴を行番号付きで取得
|
|
125
|
+
def all_with_line_numbers
|
|
126
|
+
@entries.map do |e|
|
|
127
|
+
if e.is_ai_question
|
|
128
|
+
# AI会話は質問と回答をまとめて表示
|
|
129
|
+
response_preview = if e.ai_response
|
|
130
|
+
truncate(e.ai_response, 100)
|
|
131
|
+
else
|
|
132
|
+
"(回答待ち)"
|
|
133
|
+
end
|
|
134
|
+
"#{e.line_no}: [USER] #{e.code} => [AI] #{response_preview}"
|
|
135
|
+
else
|
|
136
|
+
"#{e.line_no}: #{e.code}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def truncate(str, max_length)
|
|
142
|
+
str.length > max_length ? str[0, max_length] + "..." : str
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# メソッド定義のインデックス(メソッド名: 行範囲)
|
|
146
|
+
def method_index
|
|
147
|
+
@method_definitions.map do |m|
|
|
148
|
+
if m.start_line == m.end_line
|
|
149
|
+
"#{m.name}: #{m.start_line}行目"
|
|
150
|
+
else
|
|
151
|
+
"#{m.name}: #{m.start_line}-#{m.end_line}行目"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# AI質問への回答と思考の過程を記録
|
|
157
|
+
def record_ai_response(line_no, response, reasoning = nil)
|
|
158
|
+
entry = find_by_line(line_no)
|
|
159
|
+
return unless entry
|
|
160
|
+
|
|
161
|
+
entry.ai_response = response
|
|
162
|
+
entry.ai_reasoning = reasoning
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# 特定の行のAI詳細情報を取得
|
|
166
|
+
def get_ai_detail(line_no)
|
|
167
|
+
entry = find_by_line(line_no)
|
|
168
|
+
return nil unless entry&.ai_response
|
|
169
|
+
|
|
170
|
+
{
|
|
171
|
+
line_no: entry.line_no,
|
|
172
|
+
question: entry.code,
|
|
173
|
+
response: entry.ai_response,
|
|
174
|
+
reasoning: entry.ai_reasoning
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# AI会話の一覧(質問と回答のみ、思考の過程は含まない)
|
|
179
|
+
def ai_conversations
|
|
180
|
+
@entries.select { |e| e.is_ai_question && e.ai_response }.map do |e|
|
|
181
|
+
{
|
|
182
|
+
line_no: e.line_no,
|
|
183
|
+
question: e.code,
|
|
184
|
+
response: e.ai_response
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def extract_method_name(def_line)
|
|
192
|
+
# "def foo" や "def foo(bar)" からメソッド名を抽出
|
|
193
|
+
match = def_line.match(/def\s+(\w+[?!=]?)/)
|
|
194
|
+
match ? match[1] : "unknown"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
module Tools
|
|
5
|
+
class Base
|
|
6
|
+
class << self
|
|
7
|
+
def tool_name
|
|
8
|
+
name.split("::").last.gsub(/([A-Z])/) { "_#{$1.downcase}" }.sub(/^_/, "")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def description
|
|
12
|
+
raise NotImplementedError, "#{name} must implement .description"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parameters
|
|
16
|
+
raise NotImplementedError, "#{name} must implement .parameters"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_gemini_tool
|
|
20
|
+
{
|
|
21
|
+
name: tool_name,
|
|
22
|
+
description: description,
|
|
23
|
+
parameters: parameters
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def available?
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def execute(binding, **params)
|
|
33
|
+
raise NotImplementedError, "#{self.class.name} must implement #execute"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
protected
|
|
37
|
+
|
|
38
|
+
def safe_inspect(obj, max_length: 1000)
|
|
39
|
+
if defined?(ActiveRecord::Base) && obj.is_a?(ActiveRecord::Base)
|
|
40
|
+
return inspect_active_record(obj)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
result = obj.inspect
|
|
44
|
+
result.length > max_length ? "#{result[0, max_length]}..." : result
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
"#<#{obj.class} (inspect failed: #{e.message})>"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def inspect_active_record(obj)
|
|
50
|
+
{
|
|
51
|
+
class: obj.class.name,
|
|
52
|
+
id: obj.try(:id),
|
|
53
|
+
attributes: obj.attributes,
|
|
54
|
+
new_record: obj.new_record?,
|
|
55
|
+
changed: obj.changed?,
|
|
56
|
+
errors: obj.errors.full_messages
|
|
57
|
+
}.to_s
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Girb
|
|
6
|
+
module Tools
|
|
7
|
+
class EvaluateCode < Base
|
|
8
|
+
class << self
|
|
9
|
+
def description
|
|
10
|
+
"Execute arbitrary Ruby code in the current context and return the result. " \
|
|
11
|
+
"Use this to call methods, create objects, test conditions, or perform any Ruby operation."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parameters
|
|
15
|
+
{
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
code: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Ruby code to execute (e.g., 'user.valid?', 'Order.where(status: :pending).count', 'arr.map { |x| x * 2 }')"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
required: ["code"]
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def execute(binding, code:)
|
|
29
|
+
result = binding.eval(code)
|
|
30
|
+
{
|
|
31
|
+
code: code,
|
|
32
|
+
result: safe_inspect(result),
|
|
33
|
+
result_class: result.class.name,
|
|
34
|
+
success: true
|
|
35
|
+
}
|
|
36
|
+
rescue SyntaxError => e
|
|
37
|
+
{ code: code, error: "Syntax error: #{e.message}", success: false }
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
{ code: code, error: "#{e.class}: #{e.message}", backtrace: e.backtrace&.first(5), success: false }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Girb
|
|
6
|
+
module Tools
|
|
7
|
+
class FindFile < Base
|
|
8
|
+
MAX_RESULTS = 20
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def description
|
|
12
|
+
"Find files in the application by name pattern. " \
|
|
13
|
+
"Useful for locating models, controllers, views, or any files. " \
|
|
14
|
+
"Supports glob patterns like '*.rb' or '**/*user*.rb'."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parameters
|
|
18
|
+
{
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
pattern: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "File name pattern (glob). Examples: 'user.rb', '**/user*.rb', 'app/models/*.rb'"
|
|
24
|
+
},
|
|
25
|
+
directory: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Directory to search in (optional, defaults to app root)"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
required: ["pattern"]
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def execute(binding, pattern:, directory: nil)
|
|
36
|
+
base_dir = resolve_base_directory(directory)
|
|
37
|
+
|
|
38
|
+
unless Dir.exist?(base_dir)
|
|
39
|
+
return { error: "Directory not found: #{base_dir}" }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# パターンにディレクトリが含まれていなければ再帰検索
|
|
43
|
+
search_pattern = if pattern.include?("/") || pattern.include?("**/")
|
|
44
|
+
File.join(base_dir, pattern)
|
|
45
|
+
else
|
|
46
|
+
File.join(base_dir, "**", pattern)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
files = Dir.glob(search_pattern).reject { |f| File.directory?(f) }
|
|
50
|
+
|
|
51
|
+
# 結果を制限
|
|
52
|
+
truncated = files.length > MAX_RESULTS
|
|
53
|
+
files = files.first(MAX_RESULTS)
|
|
54
|
+
|
|
55
|
+
# 相対パスに変換
|
|
56
|
+
relative_files = files.map { |f| f.sub("#{base_dir}/", "") }
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
pattern: pattern,
|
|
60
|
+
base_directory: base_dir,
|
|
61
|
+
files: relative_files,
|
|
62
|
+
count: relative_files.length,
|
|
63
|
+
truncated: truncated
|
|
64
|
+
}
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
{ error: "#{e.class}: #{e.message}" }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def resolve_base_directory(directory)
|
|
72
|
+
if directory
|
|
73
|
+
return directory if directory.start_with?("/")
|
|
74
|
+
|
|
75
|
+
base = app_root
|
|
76
|
+
File.join(base, directory)
|
|
77
|
+
else
|
|
78
|
+
app_root
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def app_root
|
|
83
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
84
|
+
Rails.root.to_s
|
|
85
|
+
elsif defined?(Bundler) && Bundler.respond_to?(:root) && Bundler.root
|
|
86
|
+
Bundler.root.to_s
|
|
87
|
+
else
|
|
88
|
+
Dir.pwd
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|