ruby_todo 1.0.0 → 1.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e9f1ea05d4097484fbc96f725c5e505aef99414e51e5be3be40302955d800e6
4
- data.tar.gz: '0063384d1426794282c842c7f007d55d5567e275e836dd429e869923e062fa63'
3
+ metadata.gz: 342db9ba03efc1c1dce6d88189c62f4e137155e5495e1ad97d7f88df91bc0542
4
+ data.tar.gz: 6dee74213c99437b972b0267391ecf62561da217e167b07626cb4be9b1ffb254
5
5
  SHA512:
6
- metadata.gz: a9549aa3235c94cdd8f94422b4e273bb28e9e1ee69d642bb59ad8f1de8e301623378e561d532e33b6f5bf0da8fac00b7b25a08e3439b4aaf974d67556288aee6
7
- data.tar.gz: e4384af97060aa0ada97e143486257ac900ef1e9a7e68f40a492204213b382617360148f5879df5023d54011b276311d230d2b71c036356c7dcd5aae1c1fb3d5
6
+ metadata.gz: c4cacbfd07becb5aca147a972fa83c840e0e9d9c3db68416b922c7cbfc32e574d9bcb8ef91ebbe46b6be783fd5a2013cf5672c5fd99c95e592b84b54a0c9ba40
7
+ data.tar.gz: 91710c2fc5314a1f332694179ed556e7d834f9f6dfc2fd1e7d8af3776b331d31b3e1f0281f61b7e293c2db8d3c64c30bde1168e582eac53dc96b186bd6bffe6c
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [1.0.3] - 2025-03-28
2
+
3
+ * Manual release
4
+
5
+
6
+ ## [1.0.1] - 2025-03-28
7
+
8
+ * Manual release
9
+
10
+
1
11
  ## [1.0.0] - 2025-03-28
2
12
 
3
13
  * Manual release
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyTodo
4
+ class AddIsDefaultToNotebooks < ActiveRecord::Migration[7.2]
5
+ def change
6
+ add_column :notebooks, :is_default, :boolean, default: false
7
+ add_index :notebooks, :is_default
8
+ end
9
+ end
10
+ end
data/delete_notebooks.rb CHANGED
@@ -4,17 +4,67 @@
4
4
  require_relative "lib/ruby_todo"
5
5
  require_relative "lib/ruby_todo/database"
6
6
 
7
- # Setup the database connection
7
+ # Ensure database connection is established first
8
8
  RubyTodo::Database.setup
9
9
 
10
- # Count notebooks and tasks before deletion
11
- notebook_count = RubyTodo::Notebook.count
12
- task_count = RubyTodo::Task.count
10
+ def count_records
11
+ {
12
+ tasks: RubyTodo::Task.count,
13
+ notebooks: RubyTodo::Notebook.count,
14
+ templates: RubyTodo::Template.count
15
+ }
16
+ end
13
17
 
14
- # Delete tasks first to avoid foreign key constraint errors
15
- RubyTodo::Task.delete_all
16
- puts "Successfully deleted #{task_count} tasks."
18
+ def print_counts(counts, prefix = "")
19
+ puts "#{prefix}Tasks: #{counts[:tasks]}"
20
+ puts "#{prefix}Notebooks: #{counts[:notebooks]}"
21
+ puts "#{prefix}Templates: #{counts[:templates]}"
22
+ end
17
23
 
18
- # Then delete notebooks
19
- RubyTodo::Notebook.delete_all
20
- puts "Successfully deleted #{notebook_count} notebooks."
24
+ def reset_sqlite_sequences
25
+ connection = ActiveRecord::Base.connection
26
+ connection.tables.each do |table|
27
+ connection.execute("DELETE FROM sqlite_sequence WHERE name='#{table}'")
28
+ puts "Reset sequence counter for table: #{table}"
29
+ end
30
+ end
31
+
32
+ begin
33
+ # Get initial record counts
34
+ initial_counts = count_records
35
+ puts "\nCurrent record counts:"
36
+ print_counts(initial_counts)
37
+
38
+ puts "\nResetting database..."
39
+
40
+ # Drop all tables
41
+ ActiveRecord::Base.connection.tables.each do |table|
42
+ ActiveRecord::Base.connection.drop_table(table)
43
+ puts "Dropped table: #{table}"
44
+ end
45
+
46
+ # Recreate the database schema
47
+ puts "\nRecreating database schema..."
48
+ RubyTodo::Database.setup
49
+
50
+ # Reset sequence counters
51
+ puts "\nResetting sequence counters..."
52
+ reset_sqlite_sequences
53
+
54
+ # Verify the reset
55
+ final_counts = count_records
56
+ puts "\nFinal record counts:"
57
+ print_counts(final_counts)
58
+
59
+ puts "\nDatabase reset complete! All tables and sequence counters have been reset."
60
+ rescue ActiveRecord::ConnectionNotEstablished => e
61
+ puts "\nError: Could not establish database connection"
62
+ puts "Make sure the database directory exists at ~/.ruby_todo/"
63
+ puts "Error details: #{e.message}"
64
+ exit 1
65
+ rescue StandardError => e
66
+ puts "\nAn error occurred while resetting the database:"
67
+ puts e.message
68
+ puts e.backtrace
69
+ exit 1
70
+ end
@@ -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