ruby_todo 1.0.3 → 1.0.4
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
- data/CHANGELOG.md +5 -0
- data/README.md +60 -75
- data/ai_assistant_implementation.md +1 -1
- data/lib/ruby_todo/ai_assistant/command_processor.rb +127 -0
- data/lib/ruby_todo/ai_assistant/openai_integration.rb +178 -183
- data/lib/ruby_todo/ai_assistant/param_extractor.rb +64 -0
- data/lib/ruby_todo/ai_assistant/prompt_builder.rb +95 -0
- data/lib/ruby_todo/ai_assistant/task_creator.rb +161 -0
- data/lib/ruby_todo/cli.rb +128 -469
- data/lib/ruby_todo/commands/ai_assistant.rb +752 -287
- data/lib/ruby_todo/commands/ai_commands.rb +20 -0
- data/lib/ruby_todo/commands/notebook_commands.rb +39 -0
- data/lib/ruby_todo/commands/template_commands.rb +139 -0
- data/lib/ruby_todo/concerns/import_export.rb +138 -0
- data/lib/ruby_todo/concerns/statistics.rb +166 -0
- data/lib/ruby_todo/concerns/task_filters.rb +39 -0
- data/lib/ruby_todo/formatters/display_formatter.rb +80 -0
- data/lib/ruby_todo/models/task.rb +0 -7
- data/lib/ruby_todo/version.rb +1 -1
- data/lib/ruby_todo.rb +9 -0
- metadata +13 -8
- data/.env.template +0 -2
- data/lib/ruby_todo/ai_assistant/common_query_handler.rb +0 -378
- data/lib/ruby_todo/ai_assistant/task_management.rb +0 -331
- data/lib/ruby_todo/ai_assistant/task_search.rb +0 -365
- data/test_ai_assistant.rb +0 -55
- data/test_migration.rb +0 -55
@@ -1,331 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module RubyTodo
|
4
|
-
module TaskStatusMapping
|
5
|
-
private
|
6
|
-
|
7
|
-
def status_map
|
8
|
-
@status_map ||= {
|
9
|
-
"in_progress" => "in_progress",
|
10
|
-
"in progress" => "in_progress",
|
11
|
-
"inprogress" => "in_progress",
|
12
|
-
"in porgress" => "in_progress", # Common misspelling
|
13
|
-
"in pogress" => "in_progress", # Another misspelling
|
14
|
-
"in prog" => "in_progress", # Abbreviation
|
15
|
-
"in-progress" => "in_progress", # With hyphen
|
16
|
-
"n prgrs" => "in_progress", # Very abbreviated
|
17
|
-
"todo" => "todo",
|
18
|
-
"to do" => "todo",
|
19
|
-
"to-do" => "todo",
|
20
|
-
"done" => "done",
|
21
|
-
"complete" => "done",
|
22
|
-
"completed" => "done",
|
23
|
-
"finish" => "done",
|
24
|
-
"finished" => "done",
|
25
|
-
"archived" => "archived",
|
26
|
-
"arch" => "archived", # Abbreviation
|
27
|
-
"pending" => "todo"
|
28
|
-
}.freeze
|
29
|
-
end
|
30
|
-
|
31
|
-
def find_status_in_map(potential_status, pattern_name)
|
32
|
-
status_map.each_key do |key|
|
33
|
-
if potential_status.include?(key)
|
34
|
-
say "Found status '#{key}' in #{pattern_name}, mapping to '#{status_map[key]}'" if options[:verbose]
|
35
|
-
return status_map[key]
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# If we didn't find a match in the status map, log the attempted matches
|
40
|
-
if options[:verbose]
|
41
|
-
say "No direct status match found in status_map. Attempted matches:"
|
42
|
-
status_map.each_key do |key|
|
43
|
-
say " - Checking '#{key}' against '#{potential_status}'"
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
nil
|
48
|
-
end
|
49
|
-
|
50
|
-
def find_status_in_prompt(prompt)
|
51
|
-
# Try exact matches first anywhere in the prompt
|
52
|
-
status_map.each_key do |key|
|
53
|
-
if prompt.include?(key)
|
54
|
-
say "Found status '#{key}' mapping to '#{status_map[key]}'" if options[:verbose]
|
55
|
-
return status_map[key]
|
56
|
-
end
|
57
|
-
end
|
58
|
-
nil
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
module TaskStatusExtraction
|
63
|
-
include TaskStatusMapping
|
64
|
-
|
65
|
-
def extract_target_status(prompt)
|
66
|
-
prompt = prompt.downcase.strip
|
67
|
-
say "\n=== Extracting Target Status ===" if options[:verbose]
|
68
|
-
say "Looking for status in: '#{prompt}'" if options[:verbose]
|
69
|
-
|
70
|
-
# Try each pattern matcher in sequence
|
71
|
-
status = extract_status_from_related_to_pattern(prompt) ||
|
72
|
-
extract_status_from_set_status_pattern(prompt) ||
|
73
|
-
extract_status_from_to_pattern(prompt) ||
|
74
|
-
extract_status_from_as_pattern(prompt) ||
|
75
|
-
extract_status_from_general_pattern(prompt)
|
76
|
-
|
77
|
-
say "No status found in the prompt" if options[:verbose] && status.nil?
|
78
|
-
status
|
79
|
-
end
|
80
|
-
|
81
|
-
private
|
82
|
-
|
83
|
-
def extract_status_from_related_to_pattern(prompt)
|
84
|
-
return unless prompt =~ /\b(?:related\s+to|about)\b.*\bto\s+([a-z_\s]+)\b/i
|
85
|
-
|
86
|
-
potential_status = Regexp.last_match(1).strip
|
87
|
-
say "Found potential status in 'related to' pattern: '#{potential_status}'" if options[:verbose]
|
88
|
-
say "Full match: #{Regexp.last_match(0)}" if options[:verbose]
|
89
|
-
say "Captured group: #{Regexp.last_match(1)}" if options[:verbose]
|
90
|
-
|
91
|
-
find_status_in_map(potential_status, "'related to' pattern")
|
92
|
-
end
|
93
|
-
|
94
|
-
def extract_status_from_set_status_pattern(prompt)
|
95
|
-
return unless prompt =~ /\bset\s+(?:the\s+)?status\s+of\s+(?:tasks|task).*\bto\s+([a-z_\s]+)\b/i
|
96
|
-
|
97
|
-
potential_status = Regexp.last_match(1).strip
|
98
|
-
say "Found potential status in 'set status of tasks' pattern: '#{potential_status}'" if options[:verbose]
|
99
|
-
say "Full match: #{Regexp.last_match(0)}" if options[:verbose]
|
100
|
-
say "Captured group: #{Regexp.last_match(1)}" if options[:verbose]
|
101
|
-
|
102
|
-
find_status_in_map(potential_status, "'set status of tasks' pattern")
|
103
|
-
end
|
104
|
-
|
105
|
-
def extract_status_from_to_pattern(prompt)
|
106
|
-
return unless prompt =~ /\b(?:to|into|as)\s+([a-z_\s]+)\b/i
|
107
|
-
|
108
|
-
potential_status = Regexp.last_match(1).strip
|
109
|
-
say "Found potential status indicator: '#{potential_status}'" if options[:verbose]
|
110
|
-
say "Full match: #{Regexp.last_match(0)}" if options[:verbose]
|
111
|
-
say "Captured group: #{Regexp.last_match(1)}" if options[:verbose]
|
112
|
-
|
113
|
-
status = find_status_in_map(potential_status, "status indicator")
|
114
|
-
return status if status
|
115
|
-
|
116
|
-
# If we have a potential status but no exact match, try fuzzy matching
|
117
|
-
if potential_status =~ /\bin\s*p|\bn\s*p/i
|
118
|
-
say "Found fuzzy match for 'in_progress' in potential status: '#{potential_status}'" if options[:verbose]
|
119
|
-
return "in_progress"
|
120
|
-
end
|
121
|
-
|
122
|
-
nil
|
123
|
-
end
|
124
|
-
|
125
|
-
def extract_status_from_as_pattern(prompt)
|
126
|
-
return unless prompt =~ /\bas\s+([a-z_\s]+)\b/i
|
127
|
-
|
128
|
-
potential_status = Regexp.last_match(1).strip
|
129
|
-
say "Found 'as X' pattern with potential status: '#{potential_status}'" if options[:verbose]
|
130
|
-
|
131
|
-
find_status_in_map(potential_status, "'as X' pattern")
|
132
|
-
end
|
133
|
-
|
134
|
-
def extract_status_from_general_pattern(prompt)
|
135
|
-
find_status_in_prompt(prompt) ||
|
136
|
-
find_status_from_fuzzy_match(prompt) ||
|
137
|
-
find_status_from_special_chars(prompt) ||
|
138
|
-
find_status_from_final_check(prompt)
|
139
|
-
end
|
140
|
-
|
141
|
-
def find_status_from_fuzzy_match(prompt)
|
142
|
-
# Advanced fuzzy matching for progress with typos
|
143
|
-
if prompt =~ /\bin\s*p[o|r][r|o]?g(?:r?e?s{1,2})\b/i
|
144
|
-
say "Found advanced fuzzy match for 'in_progress'" if options[:verbose]
|
145
|
-
return "in_progress"
|
146
|
-
end
|
147
|
-
|
148
|
-
# Even more flexible matching for progress
|
149
|
-
if prompt =~ /\bn\s*p[r|o]?g|\bin\s*p[r|o]?g|\bn\s*p|\bin\s*p/i
|
150
|
-
say "Found flexible fuzzy match for 'in_progress'" if options[:verbose]
|
151
|
-
return "in_progress"
|
152
|
-
end
|
153
|
-
nil
|
154
|
-
end
|
155
|
-
|
156
|
-
def find_status_from_special_chars(prompt)
|
157
|
-
return nil unless prompt.end_with?(" in", " to", "\"", "'", "\n")
|
158
|
-
|
159
|
-
clean_prompt = prompt.gsub(/["'\n]/, " ").strip
|
160
|
-
say "Cleaned prompt for status detection: '#{clean_prompt}'" if options[:verbose]
|
161
|
-
|
162
|
-
case clean_prompt
|
163
|
-
when /prog|porg|p[o|r]g/i
|
164
|
-
say "Inferring in_progress from context" if options[:verbose]
|
165
|
-
"in_progress"
|
166
|
-
when /\btodo\b|\bto do\b|pending/i
|
167
|
-
say "Inferring todo from context" if options[:verbose]
|
168
|
-
"todo"
|
169
|
-
when /\bdone\b|complete/i
|
170
|
-
say "Inferring done from context" if options[:verbose]
|
171
|
-
"done"
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
def find_status_from_final_check(prompt)
|
176
|
-
# Final attempt to match "in progress" pattern
|
177
|
-
if prompt =~ /\b(?:to|into|as)\s+(?:in\s+progress|in-progress|inprogress)\b/i
|
178
|
-
say "Found 'in progress' pattern in final check" if options[:verbose]
|
179
|
-
return "in_progress"
|
180
|
-
end
|
181
|
-
nil
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
module TaskMovementDetection
|
186
|
-
def should_handle_task_movement?(prompt)
|
187
|
-
prompt = prompt.downcase
|
188
|
-
say "\nChecking if should handle task movement for prompt: '#{prompt}'" if options[:verbose]
|
189
|
-
|
190
|
-
result = (prompt.include?("move") && !prompt.include?("task")) ||
|
191
|
-
(prompt.include?("move") && prompt.include?("task")) ||
|
192
|
-
(prompt.include?("change") && prompt.include?("status")) ||
|
193
|
-
(prompt.include?("change") && prompt.include?("to")) ||
|
194
|
-
(prompt.include?("set") && prompt.include?("status")) ||
|
195
|
-
(prompt =~ /\bset\s+(?:the\s+)?status\b/) ||
|
196
|
-
(prompt =~ /\bupdate\s+(?:the\s+)?(?:task|tasks).+\bto\s+\w+\b/) ||
|
197
|
-
(prompt =~ /\bmark\s+(?:all\s+)?(?:the\s+)?(?:task|tasks)?.+\b(?:as|to)\s+\w+\b/)
|
198
|
-
|
199
|
-
say "Should handle task movement: #{result}" if options[:verbose]
|
200
|
-
result
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
module TaskProcessing
|
205
|
-
def handle_matching_tasks(matching_tasks, context)
|
206
|
-
return unless matching_tasks && !matching_tasks.empty?
|
207
|
-
|
208
|
-
context[:matching_tasks] = matching_tasks
|
209
|
-
return unless options[:verbose]
|
210
|
-
|
211
|
-
say "Found #{matching_tasks.size} matching tasks:".blue
|
212
|
-
matching_tasks.each do |task|
|
213
|
-
say " - Notebook: #{task[:notebook]}, ID: #{task[:task_id]}, " \
|
214
|
-
"Title: #{task[:title]}, Status: #{task[:status]}".blue
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
def process_search_term(search_term, prompt, context)
|
219
|
-
say "\nSearching for tasks matching: '#{search_term}'" if options[:verbose]
|
220
|
-
say "Current context: #{context.inspect}" if options[:verbose]
|
221
|
-
|
222
|
-
matching_tasks = pre_search_tasks(search_term, prompt)
|
223
|
-
say "Pre-search returned #{matching_tasks&.size || 0} tasks" if options[:verbose]
|
224
|
-
|
225
|
-
if matching_tasks&.any?
|
226
|
-
process_matching_tasks(matching_tasks, prompt, context, search_term)
|
227
|
-
else
|
228
|
-
say "\nNo tasks found matching: '#{search_term}'".yellow
|
229
|
-
say "Context at failure: #{context.inspect}" if options[:verbose]
|
230
|
-
context[:matching_tasks] = []
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
def process_matching_tasks(matching_tasks, prompt, context, search_term)
|
235
|
-
target_status = extract_target_status(prompt)
|
236
|
-
say "\nProcessing matching tasks with target status: '#{target_status}'" if options[:verbose]
|
237
|
-
|
238
|
-
if target_status
|
239
|
-
handle_matching_tasks(matching_tasks, context)
|
240
|
-
context[:target_status] = target_status
|
241
|
-
context[:search_term] = search_term
|
242
|
-
say "\nFound #{matching_tasks.size} tasks to move to #{target_status}" if options[:verbose]
|
243
|
-
say "Updated context: #{context.inspect}" if options[:verbose]
|
244
|
-
else
|
245
|
-
say "\nCould not determine target status from prompt".yellow
|
246
|
-
say "Current context: #{context.inspect}" if options[:verbose]
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
module TaskStatusUpdate
|
252
|
-
def move_tasks_to_status(tasks, status)
|
253
|
-
if tasks && !tasks.empty?
|
254
|
-
display_tasks_to_move(tasks)
|
255
|
-
move_tasks(tasks, status)
|
256
|
-
else
|
257
|
-
say "\nNo tasks to move".yellow
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
private
|
262
|
-
|
263
|
-
def display_tasks_to_move(tasks)
|
264
|
-
return unless options[:verbose]
|
265
|
-
|
266
|
-
say "\nMoving tasks:".blue
|
267
|
-
tasks.each do |task|
|
268
|
-
say " - Task #{task[:task_id]} in notebook #{task[:notebook]}".blue
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def move_tasks(tasks, status)
|
273
|
-
tasks.each do |task|
|
274
|
-
move_single_task(task, status)
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
def move_single_task(task, status)
|
279
|
-
say "\nMoving task #{task[:task_id]} in notebook #{task[:notebook]} to #{status}".blue if options[:verbose]
|
280
|
-
|
281
|
-
# Try to find the notebook, first by name, then use default if not found
|
282
|
-
notebook = RubyTodo::Notebook.find_by(name: task[:notebook]) || RubyTodo::Notebook.default_notebook
|
283
|
-
unless notebook
|
284
|
-
say "No notebook found (neither specified nor default)".red
|
285
|
-
return
|
286
|
-
end
|
287
|
-
|
288
|
-
task_record = notebook.tasks.find_by(id: task[:task_id])
|
289
|
-
return say "Task #{task[:task_id]} not found in notebook '#{notebook.name}'".red unless task_record
|
290
|
-
|
291
|
-
if task_record.update(status: status)
|
292
|
-
say "Moved task #{task[:task_id]} to #{status}".green
|
293
|
-
else
|
294
|
-
say "Error moving task #{task[:task_id]}: #{task_record.errors.full_messages.join(", ")}".red
|
295
|
-
end
|
296
|
-
end
|
297
|
-
end
|
298
|
-
|
299
|
-
module TaskManagement
|
300
|
-
include TaskSearch
|
301
|
-
include TaskStatusExtraction
|
302
|
-
include TaskMovementDetection
|
303
|
-
include TaskProcessing
|
304
|
-
include TaskStatusUpdate
|
305
|
-
|
306
|
-
def handle_task_request(prompt, context)
|
307
|
-
say "\nHandling task request for prompt: '#{prompt}'" if options[:verbose]
|
308
|
-
|
309
|
-
return unless should_handle_task_movement?(prompt)
|
310
|
-
|
311
|
-
say "Task movement request confirmed" if options[:verbose]
|
312
|
-
|
313
|
-
search_term = extract_search_term(prompt)
|
314
|
-
say "Extracted search term: '#{search_term}'" if options[:verbose]
|
315
|
-
|
316
|
-
# Extract target status early to ensure we have it
|
317
|
-
target_status = extract_target_status(prompt)
|
318
|
-
say "Early status extraction result: '#{target_status}'" if options[:verbose]
|
319
|
-
|
320
|
-
if search_term
|
321
|
-
process_search_term(search_term, prompt, context)
|
322
|
-
|
323
|
-
# If we have a target status but it wasn't set in context, set it now
|
324
|
-
if target_status && !context[:target_status]
|
325
|
-
say "Setting target status from early extraction: '#{target_status}'" if options[:verbose]
|
326
|
-
context[:target_status] = target_status
|
327
|
-
end
|
328
|
-
end
|
329
|
-
end
|
330
|
-
end
|
331
|
-
end
|
@@ -1,365 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module RubyTodo
|
4
|
-
module TaskSearchPatterns
|
5
|
-
private
|
6
|
-
|
7
|
-
def status_of_pattern
|
8
|
-
@status_of_pattern ||= build_status_pattern
|
9
|
-
end
|
10
|
-
|
11
|
-
def move_pattern
|
12
|
-
@move_pattern ||= build_move_pattern
|
13
|
-
end
|
14
|
-
|
15
|
-
def mark_pattern
|
16
|
-
@mark_pattern ||= /^mark\s+(?:all\s+)?(?:tasks?\s+)?(?:about\s+|related\s+to\s+|concerning\s+)?/i
|
17
|
-
end
|
18
|
-
|
19
|
-
def build_status_pattern
|
20
|
-
pattern_parts = []
|
21
|
-
pattern_parts << "^(?:the\\s+)?"
|
22
|
-
pattern_parts << "status\\s+of\\s+"
|
23
|
-
pattern_parts << "(?:the\\s+)?"
|
24
|
-
pattern_parts << "(?:tasks?\\s+)?"
|
25
|
-
pattern_parts << "(?:about\\s+|related\\s+to\\s+|concerning\\s+)?"
|
26
|
-
Regexp.new(pattern_parts.join, Regexp::IGNORECASE)
|
27
|
-
end
|
28
|
-
|
29
|
-
def build_move_pattern
|
30
|
-
pattern_parts = []
|
31
|
-
pattern_parts << "^(?:move|change|update|set)\\s+"
|
32
|
-
pattern_parts << "(?:the\\s+)?"
|
33
|
-
pattern_parts << "(?:status\\s+of\\s+)?"
|
34
|
-
pattern_parts << "(?:all\\s+)?"
|
35
|
-
pattern_parts << "(?:tasks?\\s+)?"
|
36
|
-
pattern_parts << "(?:about\\s+|related\\s+to\\s+|concerning\\s+)?"
|
37
|
-
Regexp.new(pattern_parts.join, Regexp::IGNORECASE)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
module TaskSearchTermCleaner
|
42
|
-
private
|
43
|
-
|
44
|
-
def clean_search_term(search_term)
|
45
|
-
return "*" if search_term.nil? || search_term.strip.empty?
|
46
|
-
|
47
|
-
# Remove common task-related words and patterns
|
48
|
-
cleaned = remove_task_related_words(search_term)
|
49
|
-
cleaned = remove_status_related_words(cleaned)
|
50
|
-
cleaned = remove_action_words(cleaned)
|
51
|
-
cleaned = clean_special_patterns(cleaned)
|
52
|
-
|
53
|
-
cleaned.strip
|
54
|
-
end
|
55
|
-
|
56
|
-
def remove_task_related_words(term)
|
57
|
-
term.gsub(/\b(?:task|tasks|about|related to|concerning)\b/i, "")
|
58
|
-
end
|
59
|
-
|
60
|
-
def remove_status_related_words(term)
|
61
|
-
term.gsub(/\b(?:status|state)\b/i, "")
|
62
|
-
end
|
63
|
-
|
64
|
-
def remove_action_words(term)
|
65
|
-
term.gsub(/\b(?:move|change|update|set|mark)\b/i, "")
|
66
|
-
end
|
67
|
-
|
68
|
-
def clean_special_patterns(term)
|
69
|
-
# Handle "related to X" pattern
|
70
|
-
if term =~ /related\s+to\s+(.+)/i
|
71
|
-
term = Regexp.last_match(1)
|
72
|
-
end
|
73
|
-
|
74
|
-
# Clean up any remaining special characters and extra whitespace
|
75
|
-
term.gsub(/[^\w\s-]/, " ")
|
76
|
-
.gsub(/\s+/, " ")
|
77
|
-
.strip
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
module TaskSearchMatcher
|
82
|
-
private
|
83
|
-
|
84
|
-
def task_matches_any_term?(task, search_terms)
|
85
|
-
return true if search_terms == ["*"]
|
86
|
-
|
87
|
-
matching_terms = []
|
88
|
-
search_terms.each do |term|
|
89
|
-
next if term.nil? || term.strip.empty?
|
90
|
-
|
91
|
-
term = term.strip.downcase
|
92
|
-
matches = check_task_match(task, term)
|
93
|
-
matching_terms << term if matches
|
94
|
-
end
|
95
|
-
|
96
|
-
handle_compound_query_matches(task, search_terms, matching_terms)
|
97
|
-
end
|
98
|
-
|
99
|
-
def check_task_match(task, term)
|
100
|
-
matches = false
|
101
|
-
matches ||= task[:task_id].to_s == term
|
102
|
-
matches ||= task[:title].downcase.include?(term)
|
103
|
-
matches ||= task[:description]&.downcase&.include?(term)
|
104
|
-
matches ||= task[:tags]&.downcase&.include?(term)
|
105
|
-
matches ||= task[:status].downcase == term
|
106
|
-
matches ||= task[:notebook].downcase.include?(term)
|
107
|
-
matches
|
108
|
-
end
|
109
|
-
|
110
|
-
def handle_compound_query_matches(task, search_terms, matching_terms)
|
111
|
-
return true if matching_terms.any? && search_terms.size == 1
|
112
|
-
|
113
|
-
handle_or_query(task, search_terms, matching_terms) ||
|
114
|
-
handle_and_query(search_terms, matching_terms) ||
|
115
|
-
handle_related_to_query(task, search_terms) ||
|
116
|
-
matching_terms.size == search_terms.size
|
117
|
-
end
|
118
|
-
|
119
|
-
def handle_or_query(task, search_terms, _matching_terms)
|
120
|
-
return false unless or_query?
|
121
|
-
|
122
|
-
matching_terms_for_or = find_matching_terms_for_or(task, search_terms)
|
123
|
-
return false unless matching_terms_for_or.any?
|
124
|
-
|
125
|
-
log_or_query_match(matching_terms_for_or)
|
126
|
-
true
|
127
|
-
end
|
128
|
-
|
129
|
-
def or_query?
|
130
|
-
Thread.current[:original_prompt] &&
|
131
|
-
Thread.current[:original_prompt].downcase =~ /\bor\b/i
|
132
|
-
end
|
133
|
-
|
134
|
-
def find_matching_terms_for_or(task, search_terms)
|
135
|
-
matching_terms = []
|
136
|
-
search_terms.each do |term|
|
137
|
-
if check_task_match(task, term)
|
138
|
-
matching_terms << term
|
139
|
-
elsif options[:verbose]
|
140
|
-
say " Term '#{term}' does not match in OR compound query"
|
141
|
-
end
|
142
|
-
end
|
143
|
-
matching_terms
|
144
|
-
end
|
145
|
-
|
146
|
-
def log_or_query_match(matching_terms)
|
147
|
-
return unless options[:verbose]
|
148
|
-
|
149
|
-
say " Found match for OR condition with term(s): " \
|
150
|
-
"#{matching_terms.join(", ")}"
|
151
|
-
end
|
152
|
-
|
153
|
-
def handle_and_query(search_terms, matching_terms)
|
154
|
-
return false unless and_query?
|
155
|
-
|
156
|
-
matching_terms.size == search_terms.size
|
157
|
-
end
|
158
|
-
|
159
|
-
def and_query?
|
160
|
-
Thread.current[:original_prompt] &&
|
161
|
-
Thread.current[:original_prompt].downcase =~ /\band\b/i
|
162
|
-
end
|
163
|
-
|
164
|
-
def handle_related_to_query(task, _search_terms)
|
165
|
-
return false unless related_to_query?
|
166
|
-
|
167
|
-
related_terms = extract_related_terms
|
168
|
-
return false unless related_terms
|
169
|
-
|
170
|
-
matching_terms = find_matching_terms_for_related(task, related_terms)
|
171
|
-
return false unless matching_terms.any?
|
172
|
-
|
173
|
-
log_related_to_match(matching_terms)
|
174
|
-
true
|
175
|
-
end
|
176
|
-
|
177
|
-
def related_to_query?
|
178
|
-
Thread.current[:original_prompt] =~ /related\s+to\s+(.+)/i
|
179
|
-
end
|
180
|
-
|
181
|
-
def extract_related_terms
|
182
|
-
return unless Thread.current[:original_prompt] =~ /related\s+to\s+(.+)/i
|
183
|
-
|
184
|
-
Regexp.last_match(1).split(/\s+or\s+|\s+and\s+/).map(&:strip)
|
185
|
-
end
|
186
|
-
|
187
|
-
def find_matching_terms_for_related(task, related_terms)
|
188
|
-
matching_terms = []
|
189
|
-
related_terms.each do |term|
|
190
|
-
if check_task_match(task, term)
|
191
|
-
matching_terms << term
|
192
|
-
elsif options[:verbose]
|
193
|
-
say " Term '#{term}' does not match in 'related to' query"
|
194
|
-
end
|
195
|
-
end
|
196
|
-
matching_terms
|
197
|
-
end
|
198
|
-
|
199
|
-
def log_related_to_match(matching_terms)
|
200
|
-
return unless options[:verbose]
|
201
|
-
|
202
|
-
say " Found match for 'related to' with term(s): " \
|
203
|
-
"#{matching_terms.join(", ")}"
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
module TaskSearchCore
|
208
|
-
include TaskSearchPatterns
|
209
|
-
include TaskSearchTermCleaner
|
210
|
-
include TaskSearchMatcher
|
211
|
-
|
212
|
-
def pre_search_tasks(search_term, prompt = nil)
|
213
|
-
Thread.current[:original_prompt] = prompt if prompt
|
214
|
-
|
215
|
-
if search_term == "*"
|
216
|
-
say "\nSearching for all tasks" if options[:verbose]
|
217
|
-
return find_all_tasks
|
218
|
-
end
|
219
|
-
|
220
|
-
search_terms = extract_search_terms(search_term)
|
221
|
-
find_tasks_by_search_terms(search_terms)
|
222
|
-
end
|
223
|
-
|
224
|
-
private
|
225
|
-
|
226
|
-
def find_all_tasks
|
227
|
-
# Start with the default notebook if it exists
|
228
|
-
notebooks = if RubyTodo::Notebook.default_notebook
|
229
|
-
[RubyTodo::Notebook.default_notebook]
|
230
|
-
else
|
231
|
-
RubyTodo::Notebook.all
|
232
|
-
end
|
233
|
-
|
234
|
-
tasks = []
|
235
|
-
notebooks.each do |notebook|
|
236
|
-
notebook.tasks.each do |task|
|
237
|
-
tasks << {
|
238
|
-
notebook: notebook.name,
|
239
|
-
task_id: task.id,
|
240
|
-
title: task.title,
|
241
|
-
status: task.status
|
242
|
-
}
|
243
|
-
end
|
244
|
-
end
|
245
|
-
tasks
|
246
|
-
end
|
247
|
-
|
248
|
-
def find_tasks_by_search_terms(search_terms)
|
249
|
-
matching_tasks = []
|
250
|
-
|
251
|
-
RubyTodo::Notebook.all.each do |notebook|
|
252
|
-
notebook.tasks.each do |task|
|
253
|
-
task_info = {
|
254
|
-
task_id: task.id,
|
255
|
-
title: task.title,
|
256
|
-
status: task.status,
|
257
|
-
notebook: notebook.name
|
258
|
-
}
|
259
|
-
|
260
|
-
if task_matches_any_term?(task_info, search_terms)
|
261
|
-
matching_tasks << task_info
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
matching_tasks
|
267
|
-
end
|
268
|
-
|
269
|
-
def extract_search_terms(search_term)
|
270
|
-
return [] if search_term.nil?
|
271
|
-
|
272
|
-
search_term = clean_search_term(search_term)
|
273
|
-
|
274
|
-
# Handle special cases
|
275
|
-
return ["*"] if search_term == "*" || search_term =~ /\ball\b|\bevery\b/i
|
276
|
-
|
277
|
-
# Extract terms from compound queries
|
278
|
-
terms = if search_term =~ /\band\b|\bor\b/i
|
279
|
-
search_term.split(/\s+and\s+|\s+or\s+/).map(&:strip)
|
280
|
-
else
|
281
|
-
[search_term]
|
282
|
-
end
|
283
|
-
|
284
|
-
terms.reject(&:empty?)
|
285
|
-
end
|
286
|
-
end
|
287
|
-
|
288
|
-
module TaskSearch
|
289
|
-
include TaskSearchCore
|
290
|
-
|
291
|
-
def extract_search_term(prompt)
|
292
|
-
prompt = prompt.downcase.strip
|
293
|
-
say "\nExtracting search term from prompt: '#{prompt}'" if options[:verbose]
|
294
|
-
|
295
|
-
# Handle special cases first
|
296
|
-
return "*" if prompt =~ /\b(?:all|every)\s+(?:tasks?|things?)\b/i
|
297
|
-
return "*" if prompt =~ /\bmove\s+(?:all|everything)\b/i
|
298
|
-
|
299
|
-
# Try to extract terms from different patterns
|
300
|
-
extracted_terms = extract_terms_from_patterns(prompt)
|
301
|
-
|
302
|
-
# If no pattern matched, try to extract terms from the remaining text
|
303
|
-
if extracted_terms.nil?
|
304
|
-
extracted_terms = clean_remaining_text(prompt)
|
305
|
-
end
|
306
|
-
|
307
|
-
clean_search_term(extracted_terms)
|
308
|
-
end
|
309
|
-
|
310
|
-
private
|
311
|
-
|
312
|
-
def extract_terms_from_patterns(prompt)
|
313
|
-
extract_related_to_terms(prompt) ||
|
314
|
-
extract_status_terms(prompt) ||
|
315
|
-
extract_move_terms(prompt) ||
|
316
|
-
extract_mark_terms(prompt)
|
317
|
-
end
|
318
|
-
|
319
|
-
def extract_related_to_terms(prompt)
|
320
|
-
return unless prompt =~ /related\s+to\s+(.+?)(?:\s+(?:to|into|as)\s+|$)/i
|
321
|
-
|
322
|
-
terms = Regexp.last_match(1)
|
323
|
-
say "Found 'related to' pattern with terms: '#{terms}'" if options[:verbose]
|
324
|
-
terms
|
325
|
-
end
|
326
|
-
|
327
|
-
def extract_status_terms(prompt)
|
328
|
-
return unless prompt =~ /#{status_of_pattern}(.+?)(?:\s+(?:to|into|as)\s+|$)/i
|
329
|
-
|
330
|
-
terms = Regexp.last_match(1)
|
331
|
-
say "Found status pattern with terms: '#{terms}'" if options[:verbose]
|
332
|
-
terms
|
333
|
-
end
|
334
|
-
|
335
|
-
def extract_move_terms(prompt)
|
336
|
-
# Handle task ID pattern first
|
337
|
-
if prompt =~ /\btask\s+(\d+)\s+in\s+([^"']+?)(?:\s+(?:to|into|as)\s+|$)/i
|
338
|
-
task_id = Regexp.last_match(1)
|
339
|
-
say "Found task ID pattern with ID: '#{task_id}'" if options[:verbose]
|
340
|
-
return task_id
|
341
|
-
end
|
342
|
-
|
343
|
-
# Handle other move patterns
|
344
|
-
return unless prompt =~ /#{move_pattern}(.+?)(?:\s+(?:to|into|as)\s+|$)/i
|
345
|
-
|
346
|
-
terms = Regexp.last_match(1)
|
347
|
-
say "Found move pattern with terms: '#{terms}'" if options[:verbose]
|
348
|
-
terms
|
349
|
-
end
|
350
|
-
|
351
|
-
def extract_mark_terms(prompt)
|
352
|
-
return unless prompt =~ /#{mark_pattern}(.+?)(?:\s+(?:to|into|as)\s+|$)/i
|
353
|
-
|
354
|
-
terms = Regexp.last_match(1)
|
355
|
-
say "Found mark pattern with terms: '#{terms}'" if options[:verbose]
|
356
|
-
terms
|
357
|
-
end
|
358
|
-
|
359
|
-
def clean_remaining_text(prompt)
|
360
|
-
prompt.gsub(/(?:move|change|update|set|mark)\s+(?:to|into|as)\s+\w+/, "")
|
361
|
-
.gsub(/\b(?:status|state)\b/i, "")
|
362
|
-
.strip
|
363
|
-
end
|
364
|
-
end
|
365
|
-
end
|