ruby_todo 1.0.0 → 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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/CHANGELOG.md +5 -0
- data/README.md +20 -0
- data/db/migrate/20240328_add_is_default_to_notebooks.rb +10 -0
- data/lib/ruby_todo/ai_assistant/common_query_handler.rb +378 -0
- data/lib/ruby_todo/ai_assistant/configuration_management.rb +27 -0
- data/lib/ruby_todo/ai_assistant/openai_integration.rb +333 -0
- data/lib/ruby_todo/ai_assistant/task_creation.rb +86 -0
- data/lib/ruby_todo/ai_assistant/task_management.rb +327 -0
- data/lib/ruby_todo/ai_assistant/task_search.rb +362 -0
- data/lib/ruby_todo/cli.rb +294 -149
- data/lib/ruby_todo/commands/ai_assistant.rb +341 -345
- data/lib/ruby_todo/database.rb +58 -84
- data/lib/ruby_todo/models/notebook.rb +44 -10
- data/lib/ruby_todo/version.rb +1 -1
- data/progress_ai_test.md +126 -0
- data/protectors_tasks.json +159 -0
- data/test_ai_assistant.rb +55 -0
- data/test_migration.rb +55 -0
- metadata +13 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 335ebe29df566a811eddbc67c2443bff069e45d3c6b93bc736d68a23e088358d
|
4
|
+
data.tar.gz: a3408be015a62fa83b971effc2ace94d6b02b34a26c31b13264fbab59af8f69e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c96743c0f93b769157410d51e0cad83a10bd589bd87bf69da09497e36e9fc3779578dbd4e63024fdf83a7774c06fcda05698e595911edb9e127e5f548f0e0e5
|
7
|
+
data.tar.gz: 00e42b86e41c16193c84fdda638a6ccea20ecdca4f4af7c583808e9be552189f78b1568ac649294a38e1dca9a6be828cfe2ef4f81b46c03f5365b2ba879c00cc
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -259,10 +259,30 @@ $ ruby_todo ai ask "Create a new task in my Work notebook to update the document
|
|
259
259
|
$ ruby_todo ai ask "Move all tasks related to the API project to in_progress status"
|
260
260
|
```
|
261
261
|
|
262
|
+
```bash
|
263
|
+
$ ruby_todo ai ask "Show me all high priority tasks"
|
264
|
+
```
|
265
|
+
|
262
266
|
```bash
|
263
267
|
$ ruby_todo ai ask "Create a JSON to import 5 new tasks for my upcoming vacation"
|
264
268
|
```
|
265
269
|
|
270
|
+
#### Bulk Operations
|
271
|
+
|
272
|
+
The AI assistant can perform bulk operations on all tasks:
|
273
|
+
|
274
|
+
```bash
|
275
|
+
$ ruby_todo ai ask "Move all tasks to todo"
|
276
|
+
```
|
277
|
+
|
278
|
+
```bash
|
279
|
+
$ ruby_todo ai ask "Move all tasks to in_progress"
|
280
|
+
```
|
281
|
+
|
282
|
+
```bash
|
283
|
+
$ ruby_todo ai ask "Show me all task statistics"
|
284
|
+
```
|
285
|
+
|
266
286
|
Pass in an API key directly (if not configured):
|
267
287
|
```bash
|
268
288
|
$ ruby_todo ai ask "What tasks are overdue?" --api-key=your_api_key_here --api=claude
|
@@ -0,0 +1,378 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module TaskStatistics
|
5
|
+
private
|
6
|
+
|
7
|
+
def handle_statistics_query(prompt)
|
8
|
+
say "\nHandling statistics query" if options[:verbose]
|
9
|
+
|
10
|
+
if prompt =~ /\b(?:show|display|get)\s+(?:me\s+)?(?:the\s+)?stats\b/i
|
11
|
+
display_task_statistics
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def display_task_statistics
|
19
|
+
notebooks = RubyTodo::Notebook.all
|
20
|
+
total_stats = { todo: 0, in_progress: 0, done: 0, archived: 0 }
|
21
|
+
|
22
|
+
notebooks.each do |notebook|
|
23
|
+
stats = notebook.task_statistics
|
24
|
+
total_stats.merge!(stats) { |_key, old_val, new_val| old_val + new_val }
|
25
|
+
|
26
|
+
display_notebook_statistics(notebook, stats)
|
27
|
+
end
|
28
|
+
|
29
|
+
display_total_statistics(total_stats)
|
30
|
+
end
|
31
|
+
|
32
|
+
def display_notebook_statistics(notebook, stats)
|
33
|
+
say "\nNotebook: #{notebook.name}".blue
|
34
|
+
say " Todo: #{stats[:todo]}".yellow
|
35
|
+
say " In Progress: #{stats[:in_progress]}".blue
|
36
|
+
say " Done: #{stats[:done]}".green
|
37
|
+
say " Archived: #{stats[:archived]}".gray
|
38
|
+
end
|
39
|
+
|
40
|
+
def display_total_statistics(stats)
|
41
|
+
say "\nTotal Statistics:".blue
|
42
|
+
say " Todo: #{stats[:todo]}".yellow
|
43
|
+
say " In Progress: #{stats[:in_progress]}".blue
|
44
|
+
say " Done: #{stats[:done]}".green
|
45
|
+
say " Archived: #{stats[:archived]}".gray
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module TaskPriority
|
50
|
+
private
|
51
|
+
|
52
|
+
def handle_priority_query(prompt)
|
53
|
+
say "\nHandling priority query" if options[:verbose]
|
54
|
+
|
55
|
+
if prompt =~ /\b(?:show|display|get|list)\s+(?:me\s+)?(?:the\s+)?(?:high|medium|low)\s*-?\s*priority\b/i
|
56
|
+
display_priority_tasks(prompt)
|
57
|
+
return true
|
58
|
+
end
|
59
|
+
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
def display_priority_tasks(prompt)
|
64
|
+
priority = extract_priority_level(prompt)
|
65
|
+
tasks = find_priority_tasks(priority)
|
66
|
+
|
67
|
+
if tasks.any?
|
68
|
+
display_tasks_by_priority(tasks, priority)
|
69
|
+
else
|
70
|
+
say "No #{priority} priority tasks found.".yellow
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def extract_priority_level(prompt)
|
75
|
+
if prompt =~ /\b(high|medium|low)\s*-?\s*priority\b/i
|
76
|
+
Regexp.last_match(1).downcase
|
77
|
+
else
|
78
|
+
"high" # Default to high priority
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def find_priority_tasks(priority)
|
83
|
+
tasks = []
|
84
|
+
RubyTodo::Notebook.all.each do |notebook|
|
85
|
+
notebook.tasks.each do |task|
|
86
|
+
next unless task_matches_priority?(task, priority)
|
87
|
+
|
88
|
+
tasks << {
|
89
|
+
task_id: task.id,
|
90
|
+
title: task.title,
|
91
|
+
status: task.status,
|
92
|
+
notebook: notebook.name
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
tasks
|
97
|
+
end
|
98
|
+
|
99
|
+
def task_matches_priority?(task, priority)
|
100
|
+
return false unless task.tags
|
101
|
+
|
102
|
+
case priority
|
103
|
+
when "high"
|
104
|
+
task.tags.downcase.include?("high") || task.tags.downcase.include?("urgent")
|
105
|
+
when "medium"
|
106
|
+
task.tags.downcase.include?("medium") || task.tags.downcase.include?("normal")
|
107
|
+
when "low"
|
108
|
+
task.tags.downcase.include?("low")
|
109
|
+
else
|
110
|
+
false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def display_tasks_by_priority(tasks, priority)
|
115
|
+
say "\n#{priority.capitalize} Priority Tasks:".blue
|
116
|
+
tasks.each do |task|
|
117
|
+
status_color = case task[:status]
|
118
|
+
when "todo" then :yellow
|
119
|
+
when "in_progress" then :blue
|
120
|
+
when "done" then :green
|
121
|
+
else :white
|
122
|
+
end
|
123
|
+
|
124
|
+
say " [#{task[:notebook]}] Task #{task[:task_id]}: #{task[:title]}".send(status_color)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
module TaskDeadlines
|
130
|
+
private
|
131
|
+
|
132
|
+
def handle_deadline_query(prompt)
|
133
|
+
say "\nHandling deadline query" if options[:verbose]
|
134
|
+
# rubocop:disable Layout/LineLength
|
135
|
+
deadline_pattern = /\b(?:show|display|get|list)\s+(?:me\s+)?(?:the\s+)?(?:upcoming|due|overdue)\s+(?:tasks|deadlines)\b/
|
136
|
+
# rubocop:enable Layout/LineLength
|
137
|
+
if prompt =~ /#{deadline_pattern}/i
|
138
|
+
display_deadline_tasks(prompt)
|
139
|
+
return true
|
140
|
+
end
|
141
|
+
|
142
|
+
false
|
143
|
+
end
|
144
|
+
|
145
|
+
def display_deadline_tasks(prompt)
|
146
|
+
deadline_type = extract_deadline_type(prompt)
|
147
|
+
tasks = find_deadline_tasks(deadline_type)
|
148
|
+
|
149
|
+
if tasks.any?
|
150
|
+
display_tasks_by_deadline(tasks, deadline_type)
|
151
|
+
else
|
152
|
+
say "No #{deadline_type} tasks found.".yellow
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def extract_deadline_type(prompt)
|
157
|
+
if prompt =~ /\b(upcoming|due|overdue)\b/i
|
158
|
+
Regexp.last_match(1).downcase
|
159
|
+
else
|
160
|
+
"upcoming" # Default to upcoming
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def find_deadline_tasks(deadline_type)
|
165
|
+
tasks = []
|
166
|
+
RubyTodo::Notebook.all.each do |notebook|
|
167
|
+
notebook.tasks.each do |task|
|
168
|
+
next unless task_matches_deadline?(task, deadline_type)
|
169
|
+
|
170
|
+
tasks << {
|
171
|
+
task_id: task.id,
|
172
|
+
title: task.title,
|
173
|
+
status: task.status,
|
174
|
+
notebook: notebook.name,
|
175
|
+
deadline: task.deadline
|
176
|
+
}
|
177
|
+
end
|
178
|
+
end
|
179
|
+
tasks
|
180
|
+
end
|
181
|
+
|
182
|
+
def task_matches_deadline?(task, deadline_type)
|
183
|
+
return false unless task.deadline
|
184
|
+
|
185
|
+
case deadline_type
|
186
|
+
when "upcoming"
|
187
|
+
task.deadline > Time.now && task.deadline <= Time.now + (7 * 24 * 60 * 60) # 7 days
|
188
|
+
when "due"
|
189
|
+
task.deadline <= Time.now + (24 * 60 * 60) # 1 day
|
190
|
+
when "overdue"
|
191
|
+
task.deadline < Time.now
|
192
|
+
else
|
193
|
+
false
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def display_tasks_by_deadline(tasks, deadline_type)
|
198
|
+
say "\n#{deadline_type.capitalize} Tasks:".blue
|
199
|
+
tasks.each do |task|
|
200
|
+
deadline_str = task[:deadline].strftime("%Y-%m-%d %H:%M")
|
201
|
+
status_color = case task[:status]
|
202
|
+
when "todo" then :yellow
|
203
|
+
when "in_progress" then :blue
|
204
|
+
when "done" then :green
|
205
|
+
else :white
|
206
|
+
end
|
207
|
+
|
208
|
+
say " [#{task[:notebook]}] Task #{task[:task_id]}: " \
|
209
|
+
"#{task[:title]} (Due: #{deadline_str})".send(status_color)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
module TaskCreation
|
215
|
+
private
|
216
|
+
|
217
|
+
def handle_task_creation(prompt, prompt_lower)
|
218
|
+
say "\n=== Detecting task creation request ===" if options[:verbose]
|
219
|
+
|
220
|
+
title = extract_task_title(prompt)
|
221
|
+
return false unless title
|
222
|
+
|
223
|
+
notebook_name = determine_notebook_name(prompt_lower)
|
224
|
+
return false unless notebook_name
|
225
|
+
|
226
|
+
priority = determine_priority(prompt_lower)
|
227
|
+
|
228
|
+
create_task(notebook_name, title, priority)
|
229
|
+
true
|
230
|
+
end
|
231
|
+
|
232
|
+
def extract_task_title(prompt)
|
233
|
+
# Try to extract title from quotes first
|
234
|
+
title_match = prompt.match(/'([^']+)'|"([^"]+)"/)
|
235
|
+
|
236
|
+
if title_match
|
237
|
+
title_match[1] || title_match[2]
|
238
|
+
else
|
239
|
+
# If no quoted title found, try extracting from the prompt
|
240
|
+
extract_title_from_text(prompt)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def extract_title_from_text(prompt)
|
245
|
+
potential_title = prompt
|
246
|
+
phrases_to_remove = [
|
247
|
+
"create a task", "create task", "add a task", "add task",
|
248
|
+
"called", "named", "with", "priority", "high", "medium", "low",
|
249
|
+
"in", "notebook"
|
250
|
+
]
|
251
|
+
|
252
|
+
phrases_to_remove.each do |phrase|
|
253
|
+
potential_title = potential_title.gsub(/#{phrase}/i, " ")
|
254
|
+
end
|
255
|
+
|
256
|
+
result = potential_title.strip
|
257
|
+
result.empty? ? nil : result
|
258
|
+
end
|
259
|
+
|
260
|
+
def determine_notebook_name(prompt_lower)
|
261
|
+
return nil unless Notebook.default_notebook
|
262
|
+
|
263
|
+
notebook_name = Notebook.default_notebook.name
|
264
|
+
|
265
|
+
# Try to extract a specific notebook name from the prompt
|
266
|
+
Notebook.all.each do |notebook|
|
267
|
+
if prompt_lower.include?(notebook.name.downcase)
|
268
|
+
notebook_name = notebook.name
|
269
|
+
break
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
notebook_name
|
274
|
+
end
|
275
|
+
|
276
|
+
def determine_priority(prompt_lower)
|
277
|
+
if prompt_lower.include?("high priority") || prompt_lower.match(/priority.*high/)
|
278
|
+
"high"
|
279
|
+
elsif prompt_lower.include?("medium priority") || prompt_lower.match(/priority.*medium/)
|
280
|
+
"medium"
|
281
|
+
elsif prompt_lower.include?("low priority") || prompt_lower.match(/priority.*low/)
|
282
|
+
"low"
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def create_task(notebook_name, title, priority)
|
287
|
+
say "\nCreating task in notebook: #{notebook_name}" if options[:verbose]
|
288
|
+
cli_args = ["task:add", notebook_name, title]
|
289
|
+
|
290
|
+
# Add priority if specified
|
291
|
+
cli_args.push("--priority", priority) if priority
|
292
|
+
|
293
|
+
RubyTodo::CLI.start(cli_args)
|
294
|
+
|
295
|
+
# Create a simple explanation
|
296
|
+
priority_text = priority ? " with #{priority} priority" : ""
|
297
|
+
say "\nCreated task '#{title}'#{priority_text} in the #{notebook_name} notebook"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
module CommonQueryHandler
|
302
|
+
include TaskStatistics
|
303
|
+
include TaskPriority
|
304
|
+
include TaskDeadlines
|
305
|
+
include TaskCreation
|
306
|
+
|
307
|
+
def handle_common_query(prompt)
|
308
|
+
handle_statistics_query(prompt) ||
|
309
|
+
handle_priority_query(prompt) ||
|
310
|
+
handle_deadline_query(prompt)
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
def high_priority_query?(prompt_lower)
|
316
|
+
prompt_lower.include?("high priority") ||
|
317
|
+
(prompt_lower.include?("priority") && prompt_lower.include?("high"))
|
318
|
+
end
|
319
|
+
|
320
|
+
def medium_priority_query?(prompt_lower)
|
321
|
+
prompt_lower.include?("medium priority") ||
|
322
|
+
(prompt_lower.include?("priority") && prompt_lower.include?("medium"))
|
323
|
+
end
|
324
|
+
|
325
|
+
def statistics_query?(prompt_lower)
|
326
|
+
(prompt_lower.include?("statistics") || prompt_lower.include?("stats")) &&
|
327
|
+
(prompt_lower.include?("notebook") || prompt_lower.include?("tasks"))
|
328
|
+
end
|
329
|
+
|
330
|
+
def status_tasks_query?(prompt_lower)
|
331
|
+
statuses = { "todo" => "todo", "in progress" => "in_progress", "done" => "done", "archived" => "archived" }
|
332
|
+
statuses.keys.any? { |status| prompt_lower.include?(status) }
|
333
|
+
end
|
334
|
+
|
335
|
+
def notebook_listing_query?(prompt_lower)
|
336
|
+
prompt_lower.include?("list notebooks") ||
|
337
|
+
prompt_lower.include?("show notebooks") ||
|
338
|
+
prompt_lower.include?("display notebooks")
|
339
|
+
end
|
340
|
+
|
341
|
+
def handle_status_tasks(prompt_lower)
|
342
|
+
say "\n=== Detecting status tasks request ===" if options[:verbose]
|
343
|
+
|
344
|
+
statuses = { "todo" => "todo", "in progress" => "in_progress", "done" => "done", "archived" => "archived" }
|
345
|
+
status = nil
|
346
|
+
|
347
|
+
statuses.each do |name, value|
|
348
|
+
if prompt_lower.include?(name)
|
349
|
+
status = value
|
350
|
+
break
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
return false unless status
|
355
|
+
|
356
|
+
tasks = find_tasks_by_status(status)
|
357
|
+
display_tasks_by_status(tasks, status)
|
358
|
+
true
|
359
|
+
end
|
360
|
+
|
361
|
+
def handle_notebook_listing(_prompt_lower)
|
362
|
+
say "\n=== Detecting notebook listing request ===" if options[:verbose]
|
363
|
+
|
364
|
+
notebooks = Notebook.all
|
365
|
+
if notebooks.empty?
|
366
|
+
say "No notebooks found.".yellow
|
367
|
+
return true
|
368
|
+
end
|
369
|
+
|
370
|
+
say "\nNotebooks:".blue
|
371
|
+
notebooks.each do |notebook|
|
372
|
+
say " #{notebook.name}".green
|
373
|
+
end
|
374
|
+
|
375
|
+
true
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module ConfigurationManagement
|
5
|
+
def load_api_key_from_config
|
6
|
+
config = load_config
|
7
|
+
config["openai"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def load_config
|
11
|
+
return {} unless File.exist?(config_file)
|
12
|
+
|
13
|
+
YAML.load_file(config_file) || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def save_config(key, value)
|
17
|
+
config = load_config
|
18
|
+
config[key] = value
|
19
|
+
FileUtils.mkdir_p(File.dirname(config_file))
|
20
|
+
File.write(config_file, config.to_yaml)
|
21
|
+
end
|
22
|
+
|
23
|
+
def config_file
|
24
|
+
File.join(Dir.home, ".config", "ruby_todo", "config.yml")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|