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,130 @@
1
+ # Implementation Guide: Adding AI Assistant to Ruby Todo
2
+
3
+ This guide outlines the specific steps to add AI assistant functionality to the Ruby Todo gem, allowing users to interact with the application using natural language through Claude or OpenAI.
4
+
5
+ ## Step 1: Update the gemspec file
6
+
7
+ Edit `ruby_todo.gemspec` to add the required dependencies:
8
+
9
+ ```ruby
10
+ # Runtime dependencies
11
+ spec.add_dependency "anthropic", "~> 0.1.0" # For Claude API
12
+ spec.add_dependency "ruby-openai", "~> 6.3.0" # For OpenAI API
13
+ spec.add_dependency "dotenv", "~> 2.8" # For API key management
14
+ ```
15
+
16
+ ## Step 2: Create the folder structure
17
+
18
+ ```bash
19
+ mkdir -p lib/ruby_todo/commands
20
+ ```
21
+
22
+ ## Step 3: Create the AI Assistant command file
23
+
24
+ Create a new file at `lib/ruby_todo/commands/ai_assistant.rb` with the implementation from the plan document.
25
+
26
+ ## Step 4: Update the CLI class
27
+
28
+ Edit `lib/ruby_todo/cli.rb` to add the AI Assistant command:
29
+
30
+ 1. Add the require statement at the top:
31
+ ```ruby
32
+ require_relative "commands/ai_assistant"
33
+ ```
34
+
35
+ 2. Register the subcommand inside the CLI class:
36
+ ```ruby
37
+ # Register AI Assistant subcommand
38
+ desc "ai SUBCOMMAND", "Use AI assistant"
39
+ subcommand "ai", AIAssistantCommand
40
+ ```
41
+
42
+ ## Step 5: Create .env template
43
+
44
+ Create a `.env.template` file in the project root:
45
+
46
+ ```
47
+ # Claude API key (if using Claude)
48
+ ANTHROPIC_API_KEY=your_claude_api_key_here
49
+
50
+ # OpenAI API key (if using OpenAI)
51
+ OPENAI_API_KEY=your_openai_api_key_here
52
+ ```
53
+
54
+ Add `.env` to `.gitignore` to prevent accidental check-in of API keys.
55
+
56
+ ## Step 6: Update README
57
+
58
+ Add the AI Assistant documentation to the README.md file:
59
+
60
+ 1. Add "AI Assistant" to the Features list at the top
61
+ 2. Add the full AI Assistant section after the Templates section
62
+
63
+ ## Step 7: Create tests
64
+
65
+ Create tests for the AI Assistant functionality:
66
+
67
+ 1. Create a new file `test/ai_assistant_test.rb`
68
+ 2. Add unit tests for the AI Assistant command class
69
+ 3. Add integration tests for the CLI integration
70
+ 4. Add mock tests for API interactions
71
+
72
+ ## Step 8: Install and test
73
+
74
+ 1. Install the updated gem: `bundle exec rake install`
75
+ 2. Run `ruby_todo ai configure` to set up your API key
76
+ 3. Test with a simple prompt: `ruby_todo ai ask "Create a task in the Work notebook"`
77
+
78
+ ## Usage Examples
79
+
80
+ Here are some examples of how to use the AI assistant:
81
+
82
+ 1. Configure the AI assistant:
83
+ ```bash
84
+ ruby_todo ai configure
85
+ ```
86
+
87
+ 2. Create a task using natural language:
88
+ ```bash
89
+ ruby_todo ai ask "Add a high priority task to update documentation in my Work notebook due next Friday"
90
+ ```
91
+
92
+ 3. Move tasks to a different status:
93
+ ```bash
94
+ ruby_todo ai ask "Move all tasks related to documentation in my Work notebook to in_progress"
95
+ ```
96
+
97
+ 4. Generate a JSON import file:
98
+ ```bash
99
+ ruby_todo ai ask "Create a JSON file with 5 tasks for my upcoming vacation planning"
100
+ ```
101
+
102
+ 5. Get task statistics:
103
+ ```bash
104
+ ruby_todo ai ask "Give me a summary of my Work notebook"
105
+ ```
106
+
107
+ 6. Search for specific tasks:
108
+ ```bash
109
+ ruby_todo ai ask "Find all high priority tasks that are overdue"
110
+ ```
111
+
112
+ ## Troubleshooting
113
+
114
+ 1. API key issues:
115
+ - Ensure your API key is correctly configured
116
+ - Try passing the API key directly: `ruby_todo ai ask "..." --api-key=your_key`
117
+
118
+ 2. JSON parsing errors:
119
+ - Enable verbose mode to see the raw response: `ruby_todo ai ask "..." --verbose`
120
+ - Check if the model is generating valid JSON
121
+
122
+ 3. Permission issues:
123
+ - Check file permissions on `~/.ruby_todo/ai_config.json`
124
+
125
+ 4. Missing dependencies:
126
+ - Run `bundle install` to ensure all gems are installed
127
+
128
+ 5. Command not found:
129
+ - Ensure the gem is properly installed: `gem list ruby_todo`
130
+ - Reinstall if necessary: `gem uninstall ruby_todo && bundle exec rake install`
@@ -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