ruby_todo 0.4.1 → 1.0.1

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.
@@ -0,0 +1,327 @@
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
+ notebook = RubyTodo::Notebook.find_by(name: task[:notebook])
282
+ return say "Notebook '#{task[:notebook]}' not found".red unless notebook
283
+
284
+ task_record = notebook.tasks.find_by(id: task[:task_id])
285
+ return say "Task #{task[:task_id]} not found in notebook '#{task[:notebook]}'".red unless task_record
286
+
287
+ if task_record.update(status: status)
288
+ say "Task #{task[:task_id]} moved to #{status}".green
289
+ else
290
+ say "Error moving task #{task[:task_id]}: #{task_record.errors.full_messages.join(", ")}".red
291
+ end
292
+ end
293
+ end
294
+
295
+ module TaskManagement
296
+ include TaskSearch
297
+ include TaskStatusExtraction
298
+ include TaskMovementDetection
299
+ include TaskProcessing
300
+ include TaskStatusUpdate
301
+
302
+ def handle_task_request(prompt, context)
303
+ say "\nHandling task request for prompt: '#{prompt}'" if options[:verbose]
304
+
305
+ return unless should_handle_task_movement?(prompt)
306
+
307
+ say "Task movement request confirmed" if options[:verbose]
308
+
309
+ search_term = extract_search_term(prompt)
310
+ say "Extracted search term: '#{search_term}'" if options[:verbose]
311
+
312
+ # Extract target status early to ensure we have it
313
+ target_status = extract_target_status(prompt)
314
+ say "Early status extraction result: '#{target_status}'" if options[:verbose]
315
+
316
+ if search_term
317
+ process_search_term(search_term, prompt, context)
318
+
319
+ # If we have a target status but it wasn't set in context, set it now
320
+ if target_status && !context[:target_status]
321
+ say "Setting target status from early extraction: '#{target_status}'" if options[:verbose]
322
+ context[:target_status] = target_status
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,362 @@
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
+ tasks = []
228
+ RubyTodo::Notebook.all.each do |notebook|
229
+ notebook.tasks.each do |task|
230
+ tasks << {
231
+ task_id: task.id,
232
+ title: task.title,
233
+ description: task.description,
234
+ status: task.status,
235
+ tags: task.tags,
236
+ notebook: notebook.name
237
+ }
238
+ end
239
+ end
240
+ tasks
241
+ end
242
+
243
+ def find_tasks_by_search_terms(search_terms)
244
+ matching_tasks = []
245
+
246
+ RubyTodo::Notebook.all.each do |notebook|
247
+ notebook.tasks.each do |task|
248
+ task_info = {
249
+ task_id: task.id,
250
+ title: task.title,
251
+ description: task.description,
252
+ status: task.status,
253
+ tags: task.tags,
254
+ notebook: notebook.name
255
+ }
256
+
257
+ if task_matches_any_term?(task_info, search_terms)
258
+ matching_tasks << task_info
259
+ end
260
+ end
261
+ end
262
+
263
+ matching_tasks
264
+ end
265
+
266
+ def extract_search_terms(search_term)
267
+ return [] if search_term.nil?
268
+
269
+ search_term = clean_search_term(search_term)
270
+
271
+ # Handle special cases
272
+ return ["*"] if search_term == "*" || search_term =~ /\ball\b|\bevery\b/i
273
+
274
+ # Extract terms from compound queries
275
+ terms = if search_term =~ /\band\b|\bor\b/i
276
+ search_term.split(/\s+and\s+|\s+or\s+/).map(&:strip)
277
+ else
278
+ [search_term]
279
+ end
280
+
281
+ terms.reject(&:empty?)
282
+ end
283
+ end
284
+
285
+ module TaskSearch
286
+ include TaskSearchCore
287
+
288
+ def extract_search_term(prompt)
289
+ prompt = prompt.downcase.strip
290
+ say "\nExtracting search term from prompt: '#{prompt}'" if options[:verbose]
291
+
292
+ # Handle special cases first
293
+ return "*" if prompt =~ /\b(?:all|every)\s+(?:tasks?|things?)\b/i
294
+ return "*" if prompt =~ /\bmove\s+(?:all|everything)\b/i
295
+
296
+ # Try to extract terms from different patterns
297
+ extracted_terms = extract_terms_from_patterns(prompt)
298
+
299
+ # If no pattern matched, try to extract terms from the remaining text
300
+ if extracted_terms.nil?
301
+ extracted_terms = clean_remaining_text(prompt)
302
+ end
303
+
304
+ clean_search_term(extracted_terms)
305
+ end
306
+
307
+ private
308
+
309
+ def extract_terms_from_patterns(prompt)
310
+ extract_related_to_terms(prompt) ||
311
+ extract_status_terms(prompt) ||
312
+ extract_move_terms(prompt) ||
313
+ extract_mark_terms(prompt)
314
+ end
315
+
316
+ def extract_related_to_terms(prompt)
317
+ return unless prompt =~ /related\s+to\s+(.+?)(?:\s+(?:to|into|as)\s+|$)/i
318
+
319
+ terms = Regexp.last_match(1)
320
+ say "Found 'related to' pattern with terms: '#{terms}'" if options[:verbose]
321
+ terms
322
+ end
323
+
324
+ def extract_status_terms(prompt)
325
+ return unless prompt =~ /#{status_of_pattern}(.+?)(?:\s+(?:to|into|as)\s+|$)/i
326
+
327
+ terms = Regexp.last_match(1)
328
+ say "Found status pattern with terms: '#{terms}'" if options[:verbose]
329
+ terms
330
+ end
331
+
332
+ def extract_move_terms(prompt)
333
+ # Handle task ID pattern first
334
+ if prompt =~ /\btask\s+(\d+)\s+in\s+([^"']+?)(?:\s+(?:to|into|as)\s+|$)/i
335
+ task_id = Regexp.last_match(1)
336
+ say "Found task ID pattern with ID: '#{task_id}'" if options[:verbose]
337
+ return task_id
338
+ end
339
+
340
+ # Handle other move patterns
341
+ return unless prompt =~ /#{move_pattern}(.+?)(?:\s+(?:to|into|as)\s+|$)/i
342
+
343
+ terms = Regexp.last_match(1)
344
+ say "Found move pattern with terms: '#{terms}'" if options[:verbose]
345
+ terms
346
+ end
347
+
348
+ def extract_mark_terms(prompt)
349
+ return unless prompt =~ /#{mark_pattern}(.+?)(?:\s+(?:to|into|as)\s+|$)/i
350
+
351
+ terms = Regexp.last_match(1)
352
+ say "Found mark pattern with terms: '#{terms}'" if options[:verbose]
353
+ terms
354
+ end
355
+
356
+ def clean_remaining_text(prompt)
357
+ prompt.gsub(/(?:move|change|update|set|mark)\s+(?:to|into|as)\s+\w+/, "")
358
+ .gsub(/\b(?:status|state)\b/i, "")
359
+ .strip
360
+ end
361
+ end
362
+ end