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 +4 -4
- data/.rspec +1 -0
- data/CHANGELOG.md +10 -0
- data/README.md +20 -0
- data/db/migrate/20240328_add_is_default_to_notebooks.rb +10 -0
- data/delete_notebooks.rb +60 -10
- 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 +337 -0
- data/lib/ruby_todo/ai_assistant/task_creation.rb +86 -0
- data/lib/ruby_todo/ai_assistant/task_management.rb +331 -0
- data/lib/ruby_todo/ai_assistant/task_search.rb +365 -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
data/lib/ruby_todo/cli.rb
CHANGED
@@ -7,6 +7,7 @@ require "tty-table"
|
|
7
7
|
require "time"
|
8
8
|
require "json"
|
9
9
|
require "fileutils"
|
10
|
+
require "csv"
|
10
11
|
require_relative "models/notebook"
|
11
12
|
require_relative "models/task"
|
12
13
|
require_relative "models/template"
|
@@ -14,8 +15,146 @@ require_relative "database"
|
|
14
15
|
require_relative "commands/ai_assistant"
|
15
16
|
|
16
17
|
module RubyTodo
|
18
|
+
module Export
|
19
|
+
private
|
20
|
+
|
21
|
+
def export_notebook(notebook)
|
22
|
+
{
|
23
|
+
"name" => notebook.name,
|
24
|
+
"created_at" => notebook.created_at,
|
25
|
+
"updated_at" => notebook.updated_at,
|
26
|
+
"tasks" => notebook.tasks.map { |task| task_to_hash(task) }
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def export_all_notebooks(notebooks)
|
31
|
+
{
|
32
|
+
"notebooks" => notebooks.map { |notebook| export_notebook(notebook) }
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def task_to_hash(task)
|
37
|
+
{
|
38
|
+
"title" => task.title,
|
39
|
+
"description" => task.description,
|
40
|
+
"status" => task.status,
|
41
|
+
"priority" => task.priority,
|
42
|
+
"tags" => task.tags,
|
43
|
+
"due_date" => task.due_date&.iso8601,
|
44
|
+
"created_at" => task.created_at&.iso8601,
|
45
|
+
"updated_at" => task.updated_at&.iso8601
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def export_to_json(data, filename)
|
50
|
+
File.write(filename, JSON.pretty_generate(data))
|
51
|
+
end
|
52
|
+
|
53
|
+
def export_to_csv(data, filename)
|
54
|
+
CSV.open(filename, "wb") do |csv|
|
55
|
+
if data["notebooks"]
|
56
|
+
export_multiple_notebooks_to_csv(data, csv)
|
57
|
+
else
|
58
|
+
export_single_notebook_to_csv(data, csv)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def export_multiple_notebooks_to_csv(data, csv)
|
64
|
+
csv << ["Notebook", "Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date",
|
65
|
+
"Created At", "Updated At"]
|
66
|
+
|
67
|
+
data["notebooks"].each do |notebook|
|
68
|
+
notebook_name = notebook["name"]
|
69
|
+
notebook["tasks"].each_with_index do |task, index|
|
70
|
+
csv << [
|
71
|
+
notebook_name,
|
72
|
+
index + 1,
|
73
|
+
task["title"],
|
74
|
+
task["description"],
|
75
|
+
task["status"],
|
76
|
+
task["priority"],
|
77
|
+
task["tags"],
|
78
|
+
task["due_date"],
|
79
|
+
task["created_at"],
|
80
|
+
task["updated_at"]
|
81
|
+
]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def export_single_notebook_to_csv(data, csv)
|
87
|
+
csv << ["Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date", "Created At",
|
88
|
+
"Updated At"]
|
89
|
+
|
90
|
+
data["tasks"].each_with_index do |task, index|
|
91
|
+
csv << [
|
92
|
+
index + 1,
|
93
|
+
task["title"],
|
94
|
+
task["description"],
|
95
|
+
task["status"],
|
96
|
+
task["priority"],
|
97
|
+
task["tags"],
|
98
|
+
task["due_date"],
|
99
|
+
task["created_at"],
|
100
|
+
task["updated_at"]
|
101
|
+
]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
module Import
|
107
|
+
private
|
108
|
+
|
109
|
+
def import_tasks(notebook, tasks_data)
|
110
|
+
count = 0
|
111
|
+
|
112
|
+
tasks_data.each do |task_data|
|
113
|
+
# Convert ISO8601 string to Time object
|
114
|
+
due_date = Time.parse(task_data["due_date"]) if task_data["due_date"]
|
115
|
+
|
116
|
+
task = Task.create(
|
117
|
+
notebook: notebook,
|
118
|
+
title: task_data["title"],
|
119
|
+
description: task_data["description"],
|
120
|
+
status: task_data["status"] || "todo",
|
121
|
+
priority: task_data["priority"],
|
122
|
+
tags: task_data["tags"],
|
123
|
+
due_date: due_date
|
124
|
+
)
|
125
|
+
|
126
|
+
count += 1 if task.persisted?
|
127
|
+
end
|
128
|
+
|
129
|
+
count
|
130
|
+
end
|
131
|
+
|
132
|
+
def import_all_notebooks(data)
|
133
|
+
results = { notebooks: 0, tasks: 0 }
|
134
|
+
|
135
|
+
data["notebooks"].each do |notebook_data|
|
136
|
+
notebook_name = notebook_data["name"]
|
137
|
+
notebook = Notebook.find_by(name: notebook_name)
|
138
|
+
|
139
|
+
unless notebook
|
140
|
+
notebook = Notebook.create(name: notebook_name)
|
141
|
+
results[:notebooks] += 1 if notebook.persisted?
|
142
|
+
end
|
143
|
+
|
144
|
+
if notebook.persisted?
|
145
|
+
tasks_count = import_tasks(notebook, notebook_data["tasks"])
|
146
|
+
results[:tasks] += tasks_count
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
results
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
17
154
|
class CLI < Thor
|
18
155
|
include Thor::Actions
|
156
|
+
include Export
|
157
|
+
include Import
|
19
158
|
|
20
159
|
map %w[--version -v] => :version
|
21
160
|
desc "version", "Show the Ruby Todo version"
|
@@ -204,23 +343,95 @@ module RubyTodo
|
|
204
343
|
say "Ruby Todo has been initialized successfully!".green
|
205
344
|
end
|
206
345
|
|
207
|
-
# Register subcommands
|
208
|
-
desc "notebook
|
209
|
-
|
346
|
+
# Register subcommands with colon format
|
347
|
+
desc "notebook:create NAME", "Create a new notebook"
|
348
|
+
def notebook_create(name)
|
349
|
+
Notebook.create(name: name)
|
350
|
+
puts "Created notebook: #{name}".green
|
351
|
+
end
|
352
|
+
|
353
|
+
desc "notebook:list", "List all notebooks"
|
354
|
+
def notebook_list
|
355
|
+
notebooks = Notebook.all
|
356
|
+
return say "No notebooks found".yellow if notebooks.empty?
|
357
|
+
|
358
|
+
table = TTY::Table.new(
|
359
|
+
header: ["ID", "Name", "Tasks", "Created At", "Default"],
|
360
|
+
rows: notebooks.map do |notebook|
|
361
|
+
[
|
362
|
+
notebook.id,
|
363
|
+
notebook.name,
|
364
|
+
notebook.tasks.count,
|
365
|
+
notebook.created_at,
|
366
|
+
notebook.is_default? ? "✓" : ""
|
367
|
+
]
|
368
|
+
end
|
369
|
+
)
|
370
|
+
puts table.render(:ascii)
|
371
|
+
end
|
372
|
+
|
373
|
+
desc "notebook:set_default NOTEBOOK", "Set a notebook as the default"
|
374
|
+
def notebook_set_default(name)
|
375
|
+
notebook = Notebook.find_by(name: name)
|
376
|
+
if notebook
|
377
|
+
notebook.make_default!
|
378
|
+
say "Successfully set '#{name}' as the default notebook".green
|
379
|
+
else
|
380
|
+
say "Notebook '#{name}' not found".red
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
desc "template:create NAME", "Create a new task template"
|
385
|
+
def template_create(name)
|
386
|
+
TemplateCommand.new.create(name)
|
387
|
+
end
|
388
|
+
|
389
|
+
desc "template:list", "List all templates"
|
390
|
+
def template_list
|
391
|
+
TemplateCommand.new.list
|
392
|
+
end
|
210
393
|
|
211
|
-
desc "template
|
212
|
-
|
394
|
+
desc "template:show NAME", "Show details of a specific template"
|
395
|
+
def template_show(name)
|
396
|
+
TemplateCommand.new.show(name)
|
397
|
+
end
|
213
398
|
|
214
|
-
|
215
|
-
|
216
|
-
|
399
|
+
desc "template:delete NAME", "Delete a template"
|
400
|
+
def template_delete(name)
|
401
|
+
TemplateCommand.new.delete(name)
|
402
|
+
end
|
403
|
+
|
404
|
+
desc "template:use NAME NOTEBOOK", "Create a task from a template in the specified notebook"
|
405
|
+
def template_use(name, notebook)
|
406
|
+
TemplateCommand.new.use(name, notebook)
|
407
|
+
end
|
217
408
|
|
218
|
-
|
409
|
+
# Register AI Assistant subcommand with colon format
|
410
|
+
desc "ai:ask PROMPT", "Ask the AI assistant to perform tasks using natural language"
|
411
|
+
method_option :api_key, type: :string, desc: "OpenAI API key"
|
412
|
+
method_option :verbose, type: :boolean, default: false, desc: "Show detailed response"
|
413
|
+
def ai_ask(*prompt_args)
|
414
|
+
prompt = prompt_args.join(" ")
|
415
|
+
ai_command.ask(prompt)
|
416
|
+
end
|
417
|
+
|
418
|
+
desc "ai:configure", "Configure the AI assistant settings"
|
419
|
+
def ai_configure
|
420
|
+
ai_command.configure
|
421
|
+
end
|
422
|
+
|
423
|
+
# Map commands to use colon format
|
424
|
+
map "notebook:list" => :notebook_list
|
425
|
+
map "ai:ask" => :ai_ask
|
426
|
+
map "ai:configure" => :ai_configure
|
427
|
+
|
428
|
+
# Task commands
|
429
|
+
desc "task:add [NOTEBOOK] [TITLE]", "Add a new task to a notebook"
|
219
430
|
method_option :description, type: :string, desc: "Task description"
|
220
431
|
method_option :due_date, type: :string, desc: "Due date (YYYY-MM-DD HH:MM)"
|
221
432
|
method_option :priority, type: :string, desc: "Priority (high, medium, low)"
|
222
433
|
method_option :tags, type: :string, desc: "Tags (comma-separated)"
|
223
|
-
def
|
434
|
+
def task_add(notebook_name, title)
|
224
435
|
notebook = Notebook.find_by(name: notebook_name)
|
225
436
|
unless notebook
|
226
437
|
say "Notebook '#{notebook_name}' not found".red
|
@@ -249,11 +460,11 @@ module RubyTodo
|
|
249
460
|
say "Priority: #{priority}" if priority
|
250
461
|
say "Tags: #{tags}" if tags
|
251
462
|
else
|
252
|
-
say "Error
|
463
|
+
say "Error adding task: #{task.errors.full_messages.join(", ")}".red
|
253
464
|
end
|
254
465
|
end
|
255
466
|
|
256
|
-
desc "task
|
467
|
+
desc "task:list [NOTEBOOK]", "List all tasks in a notebook"
|
257
468
|
method_option :status, type: :string, desc: "Filter by status (todo, in_progress, done, archived)"
|
258
469
|
method_option :priority, type: :string, desc: "Filter by priority (high, medium, low)"
|
259
470
|
method_option :due_soon, type: :boolean, desc: "Show only tasks due soon (within 24 hours)"
|
@@ -302,7 +513,7 @@ module RubyTodo
|
|
302
513
|
puts table.render(:ascii)
|
303
514
|
end
|
304
515
|
|
305
|
-
desc "task
|
516
|
+
desc "task:show [NOTEBOOK] [TASK_ID]", "Show detailed information about a task"
|
306
517
|
def task_show(notebook_name, task_id)
|
307
518
|
notebook = Notebook.find_by(name: notebook_name)
|
308
519
|
unless notebook
|
@@ -328,14 +539,14 @@ module RubyTodo
|
|
328
539
|
say "Updated: #{task.updated_at}"
|
329
540
|
end
|
330
541
|
|
331
|
-
desc "task
|
542
|
+
desc "task:edit [NOTEBOOK] [TASK_ID]", "Edit an existing task"
|
332
543
|
method_option :title, type: :string, desc: "New title"
|
333
544
|
method_option :description, type: :string, desc: "New description"
|
334
545
|
method_option :due_date, type: :string, desc: "New due date (YYYY-MM-DD HH:MM)"
|
335
546
|
method_option :priority, type: :string, desc: "New priority (high, medium, low)"
|
336
547
|
method_option :tags, type: :string, desc: "New tags (comma-separated)"
|
337
548
|
method_option :status, type: :string, desc: "New status (todo, in_progress, done, archived)"
|
338
|
-
def
|
549
|
+
def task_edit(notebook_name, task_id)
|
339
550
|
notebook = Notebook.find_by(name: notebook_name)
|
340
551
|
unless notebook
|
341
552
|
say "Notebook '#{notebook_name}' not found".red
|
@@ -371,8 +582,8 @@ module RubyTodo
|
|
371
582
|
end
|
372
583
|
end
|
373
584
|
|
374
|
-
desc "task
|
375
|
-
def
|
585
|
+
desc "task:move [NOTEBOOK] [TASK_ID] [STATUS]", "Move a task to a different status"
|
586
|
+
def task_move(notebook_name, task_id, status)
|
376
587
|
notebook = Notebook.find_by(name: notebook_name)
|
377
588
|
unless notebook
|
378
589
|
say "Notebook '#{notebook_name}' not found".red
|
@@ -392,8 +603,8 @@ module RubyTodo
|
|
392
603
|
end
|
393
604
|
end
|
394
605
|
|
395
|
-
desc "task
|
396
|
-
def
|
606
|
+
desc "task:delete [NOTEBOOK] [TASK_ID]", "Delete a task"
|
607
|
+
def task_delete(notebook_name, task_id)
|
397
608
|
notebook = Notebook.find_by(name: notebook_name)
|
398
609
|
unless notebook
|
399
610
|
say "Notebook '#{notebook_name}' not found".red
|
@@ -410,9 +621,9 @@ module RubyTodo
|
|
410
621
|
say "Deleted task #{task_id}".green
|
411
622
|
end
|
412
623
|
|
413
|
-
desc "task
|
624
|
+
desc "task:search [QUERY]", "Search for tasks across all notebooks"
|
414
625
|
method_option :notebook, type: :string, desc: "Limit search to a specific notebook"
|
415
|
-
def
|
626
|
+
def task_search(query)
|
416
627
|
notebooks = if options[:notebook]
|
417
628
|
[Notebook.find_by(name: options[:notebook])].compact
|
418
629
|
else
|
@@ -584,140 +795,70 @@ module RubyTodo
|
|
584
795
|
end
|
585
796
|
end
|
586
797
|
|
587
|
-
#
|
588
|
-
map "task:
|
589
|
-
map "task:
|
590
|
-
map "task:
|
591
|
-
map "task:edit" =>
|
592
|
-
map "task:
|
593
|
-
map "task:
|
594
|
-
map "task:search" =>
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
def task_to_hash(task)
|
614
|
-
{
|
615
|
-
"title" => task.title,
|
616
|
-
"description" => task.description,
|
617
|
-
"status" => task.status,
|
618
|
-
"priority" => task.priority,
|
619
|
-
"tags" => task.tags,
|
620
|
-
"due_date" => task.due_date&.iso8601,
|
621
|
-
"created_at" => task.created_at&.iso8601,
|
622
|
-
"updated_at" => task.updated_at&.iso8601
|
623
|
-
}
|
624
|
-
end
|
625
|
-
|
626
|
-
def export_to_json(data, filename)
|
627
|
-
File.write(filename, JSON.pretty_generate(data))
|
628
|
-
end
|
629
|
-
|
630
|
-
def export_to_csv(data, filename)
|
631
|
-
require "csv"
|
632
|
-
|
633
|
-
CSV.open(filename, "wb") do |csv|
|
634
|
-
if data["notebooks"]
|
635
|
-
# Multiple notebooks export
|
636
|
-
csv << ["Notebook", "Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date",
|
637
|
-
"Created At", "Updated At"]
|
638
|
-
|
639
|
-
data["notebooks"].each do |notebook|
|
640
|
-
notebook_name = notebook["name"]
|
641
|
-
notebook["tasks"].each_with_index do |task, index|
|
642
|
-
csv << [
|
643
|
-
notebook_name,
|
644
|
-
index + 1,
|
645
|
-
task["title"],
|
646
|
-
task["description"],
|
647
|
-
task["status"],
|
648
|
-
task["priority"],
|
649
|
-
task["tags"],
|
650
|
-
task["due_date"],
|
651
|
-
task["created_at"],
|
652
|
-
task["updated_at"]
|
653
|
-
]
|
654
|
-
end
|
655
|
-
end
|
656
|
-
else
|
657
|
-
# Single notebook export
|
658
|
-
csv << ["Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date", "Created At",
|
659
|
-
"Updated At"]
|
660
|
-
|
661
|
-
data["tasks"].each_with_index do |task, index|
|
662
|
-
csv << [
|
663
|
-
index + 1,
|
664
|
-
task["title"],
|
665
|
-
task["description"],
|
666
|
-
task["status"],
|
667
|
-
task["priority"],
|
668
|
-
task["tags"],
|
669
|
-
task["due_date"],
|
670
|
-
task["created_at"],
|
671
|
-
task["updated_at"]
|
672
|
-
]
|
673
|
-
end
|
674
|
-
end
|
798
|
+
# Map all commands to use colon format
|
799
|
+
map "task:add" => :task_add
|
800
|
+
map "task:list" => :task_list
|
801
|
+
map "task:show" => :task_show
|
802
|
+
map "task:edit" => :task_edit
|
803
|
+
map "task:move" => :task_move
|
804
|
+
map "task:delete" => :task_delete
|
805
|
+
map "task:search" => :task_search
|
806
|
+
map "notebook:create" => :notebook_create
|
807
|
+
map "notebook:list" => :notebook_list
|
808
|
+
map "notebook:set_default" => :notebook_set_default
|
809
|
+
map "template:create" => :template_create
|
810
|
+
map "template:list" => :template_list
|
811
|
+
map "template:show" => :template_show
|
812
|
+
map "template:delete" => :template_delete
|
813
|
+
map "template:use" => :template_use
|
814
|
+
|
815
|
+
# Remove old command mappings
|
816
|
+
no_commands do
|
817
|
+
def self.remove_old_commands
|
818
|
+
remove_command :create
|
819
|
+
remove_command :list
|
820
|
+
remove_command :show
|
821
|
+
remove_command :delete
|
822
|
+
remove_command :use
|
823
|
+
remove_command :ai
|
675
824
|
end
|
676
825
|
end
|
677
826
|
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
827
|
+
remove_old_commands
|
828
|
+
|
829
|
+
# Override the help command to show only colon-formatted commands
|
830
|
+
def help(command = nil)
|
831
|
+
if command.nil?
|
832
|
+
puts "Commands:"
|
833
|
+
puts " ruby_todo ai:ask PROMPT # Ask the AI assistant to perform tasks"
|
834
|
+
puts " ruby_todo ai:configure # Configure the AI assistant settings"
|
835
|
+
puts " ruby_todo notebook:create NAME # Create a new notebook"
|
836
|
+
puts " ruby_todo notebook:list # List all notebooks"
|
837
|
+
puts " ruby_todo notebook:set_default NAME # Set a notebook as the default"
|
838
|
+
puts " ruby_todo task:add [NOTEBOOK] [TITLE] # Add a new task to a notebook"
|
839
|
+
puts " ruby_todo task:list [NOTEBOOK] # List all tasks in a notebook"
|
840
|
+
puts " ruby_todo task:show [NOTEBOOK] [TASK_ID] # Show task details"
|
841
|
+
puts " ruby_todo task:edit [NOTEBOOK] [TASK_ID] # Edit a task"
|
842
|
+
puts " ruby_todo task:move [NOTEBOOK] [TASK_ID] STATUS # Move a task to a different status"
|
843
|
+
puts " ruby_todo task:delete [NOTEBOOK] [TASK_ID] # Delete a task"
|
844
|
+
puts " ruby_todo task:search [QUERY] # Search for tasks"
|
845
|
+
puts " ruby_todo template:create NAME # Create a task template"
|
846
|
+
puts " ruby_todo template:list # List all templates"
|
847
|
+
puts " ruby_todo template:show NAME # Show template details"
|
848
|
+
puts " ruby_todo template:delete NAME # Delete a template"
|
849
|
+
puts " ruby_todo template:use NAME NOTEBOOK # Use a template"
|
850
|
+
puts " ruby_todo export [NOTEBOOK] [FILENAME] # Export tasks"
|
851
|
+
puts " ruby_todo import [FILENAME] # Import tasks"
|
852
|
+
puts " ruby_todo init # Initialize todo list"
|
853
|
+
puts " ruby_todo version # Show version"
|
854
|
+
puts "\nOptions:"
|
855
|
+
puts " [--notebook=NOTEBOOK] # Specify the notebook to use"
|
856
|
+
else
|
857
|
+
super
|
696
858
|
end
|
697
|
-
|
698
|
-
count
|
699
859
|
end
|
700
860
|
|
701
|
-
|
702
|
-
results = { notebooks: 0, tasks: 0 }
|
703
|
-
|
704
|
-
data["notebooks"].each do |notebook_data|
|
705
|
-
notebook_name = notebook_data["name"]
|
706
|
-
notebook = Notebook.find_by(name: notebook_name)
|
707
|
-
|
708
|
-
unless notebook
|
709
|
-
notebook = Notebook.create(name: notebook_name)
|
710
|
-
results[:notebooks] += 1 if notebook.persisted?
|
711
|
-
end
|
712
|
-
|
713
|
-
if notebook.persisted?
|
714
|
-
tasks_count = import_tasks(notebook, notebook_data["tasks"])
|
715
|
-
results[:tasks] += tasks_count
|
716
|
-
end
|
717
|
-
end
|
718
|
-
|
719
|
-
results
|
720
|
-
end
|
861
|
+
private
|
721
862
|
|
722
863
|
def display_notebook_stats(notebook)
|
723
864
|
stats = notebook.statistics
|
@@ -861,5 +1002,9 @@ module RubyTodo
|
|
861
1002
|
|
862
1003
|
text.length > length ? "#{text[0...length]}..." : text
|
863
1004
|
end
|
1005
|
+
|
1006
|
+
def ai_command
|
1007
|
+
@ai_command ||= AIAssistantCommand.new
|
1008
|
+
end
|
864
1009
|
end
|
865
1010
|
end
|