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
@@ -5,46 +5,34 @@ require "json"
|
|
5
5
|
require "openai"
|
6
6
|
require "dotenv/load"
|
7
7
|
|
8
|
+
require_relative "../ai_assistant/task_search"
|
9
|
+
require_relative "../ai_assistant/task_management"
|
10
|
+
require_relative "../ai_assistant/openai_integration"
|
11
|
+
require_relative "../ai_assistant/configuration_management"
|
12
|
+
require_relative "../ai_assistant/common_query_handler"
|
13
|
+
|
8
14
|
module RubyTodo
|
9
15
|
class AIAssistantCommand < Thor
|
10
|
-
|
16
|
+
include TaskManagement
|
17
|
+
include OpenAIIntegration
|
18
|
+
include ConfigurationManagement
|
19
|
+
include CommonQueryHandler
|
20
|
+
|
21
|
+
desc "ai:ask [PROMPT]", "Ask the AI assistant to perform tasks using natural language"
|
11
22
|
method_option :api_key, type: :string, desc: "OpenAI API key"
|
12
23
|
method_option :verbose, type: :boolean, default: false, desc: "Show detailed response"
|
13
24
|
def ask(*prompt_args)
|
14
25
|
prompt = prompt_args.join(" ")
|
15
|
-
|
16
|
-
|
17
|
-
return
|
18
|
-
end
|
26
|
+
validate_prompt(prompt)
|
27
|
+
say "\n=== Starting AI Assistant with prompt: '#{prompt}' ===" if options[:verbose]
|
19
28
|
|
20
|
-
#
|
21
|
-
|
22
|
-
unless api_key
|
23
|
-
say "No API key found. Please provide an API key using --api-key or set OPENAI_API_KEY environment variable".red
|
24
|
-
return
|
25
|
-
end
|
26
|
-
|
27
|
-
# Set up context for the AI
|
28
|
-
context = build_context
|
29
|
+
# Direct handling for common queries
|
30
|
+
return if handle_common_query(prompt)
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
if response[:error]
|
33
|
-
say "Error: #{response[:error]}".red
|
34
|
-
return
|
35
|
-
end
|
36
|
-
|
37
|
-
# Parse the AI's response for potential actions
|
38
|
-
parse_and_execute_actions(response[:content])
|
39
|
-
|
40
|
-
# Print the AI's response if verbose mode
|
41
|
-
if options[:verbose]
|
42
|
-
say "\nAI Assistant Response:".blue
|
43
|
-
say response[:content]
|
44
|
-
end
|
32
|
+
process_ai_query(prompt)
|
45
33
|
end
|
46
34
|
|
47
|
-
desc "configure", "Configure the AI assistant settings"
|
35
|
+
desc "ai:configure", "Configure the AI assistant settings"
|
48
36
|
def configure
|
49
37
|
prompt = TTY::Prompt.new
|
50
38
|
api_key = prompt.mask("Enter your OpenAI API key:")
|
@@ -52,402 +40,410 @@ module RubyTodo
|
|
52
40
|
say "Configuration saved successfully!".green
|
53
41
|
end
|
54
42
|
|
43
|
+
def self.banner(command, _namespace = nil, _subcommand: false)
|
44
|
+
"#{basename} #{command.name}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.exit_on_failure?
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
55
51
|
private
|
56
52
|
|
57
|
-
def
|
58
|
-
|
59
|
-
|
60
|
-
{
|
61
|
-
id: notebook.id,
|
62
|
-
name: notebook.name,
|
63
|
-
task_count: notebook.tasks.count,
|
64
|
-
todo_count: notebook.tasks.todo.count,
|
65
|
-
in_progress_count: notebook.tasks.in_progress.count,
|
66
|
-
done_count: notebook.tasks.done.count,
|
67
|
-
archived_count: notebook.tasks.archived.count
|
68
|
-
}
|
69
|
-
end
|
53
|
+
def process_ai_query(prompt)
|
54
|
+
api_key = fetch_api_key
|
55
|
+
say "\nAPI key loaded successfully" if options[:verbose]
|
70
56
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
Application Information:
|
94
|
-
#{JSON.pretty_generate(context)}
|
95
|
-
|
96
|
-
Your job is to help the user manage their tasks through natural language.
|
97
|
-
You can create tasks, move tasks between statuses, and more.
|
98
|
-
|
99
|
-
IMPORTANT FORMATTING REQUIREMENTS:
|
100
|
-
- Due dates must be in "YYYY-MM-DD HH:MM" format (e.g., "2024-04-10 14:00")
|
101
|
-
- Valid priority values are ONLY: "high", "medium", or "low" (lowercase)
|
102
|
-
- Valid status values are ONLY: "todo", "in_progress", "done", "archived" (lowercase)
|
103
|
-
- If you're unsure about any values, omit them rather than guessing
|
104
|
-
|
105
|
-
When the user asks you to perform an action, generate a response in this JSON format:
|
106
|
-
{
|
107
|
-
"explanation": "Brief explanation of what you're doing",
|
108
|
-
"actions": [
|
109
|
-
{"type": "create_task", "notebook": "Work", "title": "Task title", "description": "Task description", "priority": "high", "tags": "tag1,tag2", "due_date": "2024-04-10 14:00"},
|
110
|
-
{"type": "move_task", "notebook": "Work", "task_id": 1, "status": "in_progress"},
|
111
|
-
{"type": "create_notebook", "name": "Personal"},
|
112
|
-
{"type": "generate_import_json", "notebook": "Work", "tasks": [{...task data...}]}
|
113
|
-
]
|
114
|
-
}
|
115
|
-
|
116
|
-
Action types:
|
117
|
-
- create_task: Create a new task in a notebook (requires notebook, title; optional: description, priority, tags, due_date)
|
118
|
-
- move_task: Move a task to a different status (requires notebook, task_id, status)
|
119
|
-
- create_notebook: Create a new notebook (requires name)
|
120
|
-
- generate_import_json: Generate JSON for importing tasks (requires notebook, tasks array)
|
121
|
-
- list_tasks: List tasks in a notebook (requires notebook; optional: status, priority)
|
122
|
-
- search_tasks: Search for tasks (requires query; optional: notebook)
|
123
|
-
|
124
|
-
EXTREMELY IMPORTANT:
|
125
|
-
1. Always respond with raw JSON only
|
126
|
-
2. DO NOT use markdown code blocks (like ```json)
|
127
|
-
3. DO NOT include any explanatory text before or after the JSON
|
128
|
-
4. The response must be parseable as JSON
|
129
|
-
|
130
|
-
Always validate all fields according to the requirements before including them in your response.
|
131
|
-
SYSTEM
|
132
|
-
end
|
133
|
-
|
134
|
-
def make_openai_request(client, system_message, prompt)
|
135
|
-
response = client.chat(
|
136
|
-
parameters: {
|
137
|
-
model: "gpt-4o-mini",
|
138
|
-
messages: [
|
139
|
-
{ role: "system", content: system_message },
|
140
|
-
{ role: "user", content: prompt }
|
141
|
-
],
|
142
|
-
max_tokens: 1024
|
143
|
-
}
|
144
|
-
)
|
145
|
-
{ content: response.dig("choices", 0, "message", "content") }
|
146
|
-
rescue StandardError => e
|
147
|
-
{ error: e.message }
|
148
|
-
end
|
149
|
-
|
150
|
-
def parse_and_execute_actions(response)
|
151
|
-
cleaned_response = response.gsub(/```(?:json)?\s*/, "").gsub(/```\s*$/, "")
|
152
|
-
|
153
|
-
data = JSON.parse(cleaned_response)
|
154
|
-
|
155
|
-
say data["explanation"].green if data["explanation"]
|
156
|
-
|
157
|
-
if data["actions"] && data["actions"].is_a?(Array)
|
158
|
-
data["actions"].each do |action|
|
159
|
-
execute_action(action)
|
160
|
-
end
|
161
|
-
else
|
162
|
-
say "No actions found in response".yellow
|
163
|
-
end
|
164
|
-
rescue JSON::ParserError => e
|
165
|
-
say "Couldn't parse AI response: #{e.message}".red
|
166
|
-
say "Response starts with: #{response[0..100]}..." if options[:verbose]
|
167
|
-
say "Full response: #{response}" if options[:verbose]
|
168
|
-
end
|
169
|
-
|
170
|
-
def execute_action(action)
|
171
|
-
case action["type"]
|
172
|
-
when "create_task"
|
173
|
-
create_task(action)
|
174
|
-
when "move_task"
|
175
|
-
move_task(action)
|
176
|
-
when "create_notebook"
|
177
|
-
create_notebook(action)
|
178
|
-
when "generate_import_json"
|
179
|
-
generate_import_json(action)
|
180
|
-
when "list_tasks"
|
181
|
-
list_tasks(action)
|
182
|
-
when "search_tasks"
|
183
|
-
search_tasks(action)
|
184
|
-
else
|
185
|
-
say "Unknown action type: #{action["type"]}".yellow
|
57
|
+
context = build_context
|
58
|
+
say "\nInitial context built" if options[:verbose]
|
59
|
+
|
60
|
+
# Process based on query type
|
61
|
+
process_query_by_type(prompt, context)
|
62
|
+
|
63
|
+
# Get AI response for commands and explanation
|
64
|
+
say "\n=== Querying OpenAI ===" if options[:verbose]
|
65
|
+
response = query_openai(prompt, context, api_key)
|
66
|
+
say "\nOpenAI Response received" if options[:verbose]
|
67
|
+
|
68
|
+
# Execute actions based on response
|
69
|
+
execute_actions(response, context)
|
70
|
+
end
|
71
|
+
|
72
|
+
def process_query_by_type(prompt, context)
|
73
|
+
if should_handle_task_movement?(prompt)
|
74
|
+
# Handle task movement and build context
|
75
|
+
say "\n=== Processing Task Movement Request ===" if options[:verbose]
|
76
|
+
handle_task_request(prompt, context)
|
77
|
+
elsif options[:verbose]
|
78
|
+
say "\n=== Processing Non-Movement Request ==="
|
186
79
|
end
|
187
80
|
end
|
188
81
|
|
189
|
-
def
|
190
|
-
|
191
|
-
|
192
|
-
|
82
|
+
def execute_actions(response, context)
|
83
|
+
# If we have tasks to move, do it now
|
84
|
+
if context[:matching_tasks]&.any? && context[:target_status]
|
85
|
+
say "\n=== Moving Tasks ===" if options[:verbose]
|
86
|
+
move_tasks_to_status(context[:matching_tasks], context[:target_status])
|
193
87
|
end
|
194
88
|
|
195
|
-
|
196
|
-
|
197
|
-
say "
|
198
|
-
|
89
|
+
# Execute any additional commands from the AI
|
90
|
+
if response && response["commands"]
|
91
|
+
say "\n=== Executing Additional Commands ===" if options[:verbose]
|
92
|
+
execute_commands(response)
|
199
93
|
end
|
200
94
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
notebook: notebook,
|
206
|
-
title: action["title"],
|
207
|
-
description: action["description"],
|
208
|
-
due_date: due_date,
|
209
|
-
priority: priority,
|
210
|
-
tags: action["tags"],
|
211
|
-
status: "todo"
|
212
|
-
)
|
213
|
-
|
214
|
-
if task.valid?
|
215
|
-
say "Added task: #{action["title"]}".green
|
216
|
-
else
|
217
|
-
say "Error creating task: #{task.errors.full_messages.join(", ")}".red
|
95
|
+
# Display the explanation from the AI
|
96
|
+
if response && response["explanation"]
|
97
|
+
say "\n=== AI Explanation ===" if options[:verbose]
|
98
|
+
say "\n#{response["explanation"]}"
|
218
99
|
end
|
219
100
|
end
|
220
101
|
|
221
|
-
def
|
222
|
-
return
|
102
|
+
def validate_prompt(prompt)
|
103
|
+
return if prompt && !prompt.empty?
|
223
104
|
|
224
|
-
|
225
|
-
|
226
|
-
rescue ArgumentError
|
227
|
-
say "Invalid date format '#{due_date_str}'. Using no due date.".yellow
|
228
|
-
nil
|
229
|
-
end
|
105
|
+
say "Please provide a prompt for the AI assistant".red
|
106
|
+
raise ArgumentError, "Empty prompt"
|
230
107
|
end
|
231
108
|
|
232
|
-
def
|
233
|
-
|
109
|
+
def fetch_api_key
|
110
|
+
api_key = options[:api_key] || ENV["OPENAI_API_KEY"] || load_api_key_from_config
|
111
|
+
return api_key if api_key
|
234
112
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
113
|
+
say "No API key found. Please provide an API key using --api-key or set OPENAI_API_KEY environment variable".red
|
114
|
+
raise ArgumentError, "No API key found"
|
115
|
+
end
|
116
|
+
|
117
|
+
def build_context
|
118
|
+
{ matching_tasks: [] }
|
119
|
+
end
|
120
|
+
|
121
|
+
def execute_commands(response)
|
122
|
+
return unless response["commands"].is_a?(Array)
|
123
|
+
|
124
|
+
response["commands"].each do |command|
|
125
|
+
process_command(command)
|
241
126
|
end
|
242
127
|
end
|
243
128
|
|
244
|
-
def
|
245
|
-
|
246
|
-
|
247
|
-
|
129
|
+
def process_command(command)
|
130
|
+
say "\nExecuting command: #{command}".blue if options[:verbose]
|
131
|
+
|
132
|
+
begin
|
133
|
+
# Skip if empty or nil
|
134
|
+
return if command.nil? || command.strip.empty?
|
135
|
+
|
136
|
+
# Ensure command starts with ruby_todo
|
137
|
+
return unless command.start_with?("ruby_todo")
|
138
|
+
|
139
|
+
# Process and execute the command
|
140
|
+
process_ruby_todo_command(command)
|
141
|
+
rescue StandardError => e
|
142
|
+
say "Error executing command: #{e.message}".red
|
143
|
+
say e.backtrace.join("\n").red if options[:verbose]
|
248
144
|
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def process_ruby_todo_command(command)
|
148
|
+
# Remove the ruby_todo prefix
|
149
|
+
cmd_without_prefix = command.sub(/^ruby_todo\s+/, "")
|
150
|
+
say "\nCommand without prefix: '#{cmd_without_prefix}'".blue if options[:verbose]
|
249
151
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
152
|
+
# Convert underscores to colons for all task commands
|
153
|
+
if cmd_without_prefix =~ /^task_\w+/
|
154
|
+
cmd_without_prefix = cmd_without_prefix.sub(/^task_(\w+)/, 'task:\1')
|
155
|
+
say "\nConverted underscores to colons: '#{cmd_without_prefix}'".blue if options[:verbose]
|
254
156
|
end
|
255
157
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
158
|
+
# Convert underscores to colons for notebook commands
|
159
|
+
if cmd_without_prefix =~ /^notebook_\w+/
|
160
|
+
cmd_without_prefix = cmd_without_prefix.sub(/^notebook_(\w+)/, 'notebook:\1')
|
161
|
+
say "\nConverted underscores to colons: '#{cmd_without_prefix}'".blue if options[:verbose]
|
260
162
|
end
|
261
163
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
say "
|
266
|
-
return
|
164
|
+
# Convert underscores to colons for template commands
|
165
|
+
if cmd_without_prefix =~ /^template_\w+/
|
166
|
+
cmd_without_prefix = cmd_without_prefix.sub(/^template_(\w+)/, 'template:\1')
|
167
|
+
say "\nConverted underscores to colons: '#{cmd_without_prefix}'".blue if options[:verbose]
|
267
168
|
end
|
268
169
|
|
269
|
-
|
270
|
-
|
170
|
+
# Process different command types
|
171
|
+
if cmd_without_prefix.start_with?("task:list")
|
172
|
+
execute_task_list_command(cmd_without_prefix)
|
173
|
+
elsif cmd_without_prefix.start_with?("task:search")
|
174
|
+
execute_task_search_command(cmd_without_prefix)
|
175
|
+
elsif cmd_without_prefix.start_with?("task:move")
|
176
|
+
execute_task_move_command(cmd_without_prefix)
|
271
177
|
else
|
272
|
-
|
178
|
+
execute_other_command(cmd_without_prefix)
|
273
179
|
end
|
274
180
|
end
|
275
181
|
|
276
|
-
def
|
277
|
-
|
278
|
-
|
279
|
-
return
|
280
|
-
end
|
182
|
+
def execute_task_list_command(cmd_without_prefix)
|
183
|
+
parts = cmd_without_prefix.split(/\s+/)
|
184
|
+
say "\nSplit task:list command into parts: #{parts.inspect}".blue if options[:verbose]
|
281
185
|
|
282
|
-
|
283
|
-
|
284
|
-
|
186
|
+
if parts.size >= 2
|
187
|
+
execute_task_list_with_notebook(parts)
|
188
|
+
elsif Notebook.default_notebook
|
189
|
+
execute_task_list_with_default_notebook(parts)
|
285
190
|
else
|
286
|
-
say "
|
191
|
+
say "\nNo notebook specified for task:list command".yellow
|
287
192
|
end
|
288
193
|
end
|
289
194
|
|
290
|
-
def
|
291
|
-
|
292
|
-
|
293
|
-
|
195
|
+
def execute_task_list_with_notebook(parts)
|
196
|
+
notebook_name = parts[1]
|
197
|
+
# Extract any options
|
198
|
+
options_args = []
|
199
|
+
parts[2..].each do |part|
|
200
|
+
options_args << part if part.start_with?("--")
|
294
201
|
end
|
295
202
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
{
|
302
|
-
"title" => task["title"],
|
303
|
-
"description" => task["description"],
|
304
|
-
"status" => task["status"] || "todo",
|
305
|
-
"priority" => task["priority"],
|
306
|
-
"tags" => task["tags"],
|
307
|
-
"due_date" => task["due_date"]
|
308
|
-
}
|
309
|
-
end
|
310
|
-
}
|
311
|
-
|
312
|
-
filename = action["filename"] || "#{action["notebook"].downcase.gsub(/\s+/, "_")}_tasks.json"
|
313
|
-
File.write(filename, JSON.pretty_generate(data))
|
314
|
-
say "Generated import JSON file: #{filename}".green
|
203
|
+
# Execute the task list command with the notebook name and any options
|
204
|
+
cli_args = ["task:list", notebook_name] + options_args
|
205
|
+
say "\nRunning CLI with args: #{cli_args.inspect}".blue if options[:verbose]
|
206
|
+
RubyTodo::CLI.start(cli_args)
|
207
|
+
end
|
315
208
|
|
316
|
-
|
317
|
-
|
318
|
-
|
209
|
+
def execute_task_list_with_default_notebook(parts)
|
210
|
+
cli_args = ["task:list", Notebook.default_notebook.name]
|
211
|
+
if parts.size > 1 && parts[1].start_with?("--")
|
212
|
+
cli_args << parts[1]
|
319
213
|
end
|
214
|
+
say "\nUsing default notebook for task:list with args: #{cli_args.inspect}".blue if options[:verbose]
|
215
|
+
RubyTodo::CLI.start(cli_args)
|
320
216
|
end
|
321
217
|
|
322
|
-
def
|
323
|
-
|
324
|
-
say "Missing required field 'notebook' for list_tasks".red
|
325
|
-
return
|
326
|
-
end
|
218
|
+
def execute_task_search_command(cmd_without_prefix)
|
219
|
+
parts = cmd_without_prefix.split(/\s+/, 2) # Split into command and search term
|
327
220
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
221
|
+
if parts.size >= 2
|
222
|
+
# Pass the entire search term as a single argument, not individual words
|
223
|
+
cli_args = ["task:search", parts[1]]
|
224
|
+
say "\nRunning CLI with args: #{cli_args.inspect}".blue if options[:verbose]
|
225
|
+
RubyTodo::CLI.start(cli_args)
|
226
|
+
else
|
227
|
+
say "\nNo search term provided for task:search command".yellow
|
332
228
|
end
|
229
|
+
end
|
333
230
|
|
334
|
-
|
335
|
-
|
336
|
-
tasks = tasks.where(priority: action["priority"]) if action["priority"]
|
231
|
+
def execute_task_move_command(cmd_without_prefix)
|
232
|
+
parts = cmd_without_prefix.split(/\s+/)
|
337
233
|
|
338
|
-
|
339
|
-
|
340
|
-
|
234
|
+
# Need at least task:move NOTEBOOK TASK_ID STATUS
|
235
|
+
if parts.size >= 4
|
236
|
+
notebook_name = parts[1]
|
237
|
+
task_id = parts[2]
|
238
|
+
status = parts[3]
|
239
|
+
|
240
|
+
cli_args = ["task:move", notebook_name, task_id, status]
|
241
|
+
say "\nRunning CLI with args: #{cli_args.inspect}".blue if options[:verbose]
|
242
|
+
RubyTodo::CLI.start(cli_args)
|
243
|
+
else
|
244
|
+
say "\nInvalid task:move command format. Need NOTEBOOK, TASK_ID, and STATUS".yellow
|
341
245
|
end
|
246
|
+
end
|
342
247
|
|
343
|
-
|
248
|
+
def execute_other_command(cmd_without_prefix)
|
249
|
+
# Process all other commands
|
250
|
+
cli_args = cmd_without_prefix.split(/\s+/)
|
251
|
+
say "\nRunning CLI with args: #{cli_args.inspect}".blue if options[:verbose]
|
252
|
+
RubyTodo::CLI.start(cli_args)
|
344
253
|
end
|
345
254
|
|
346
|
-
def
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
255
|
+
def handle_common_query(prompt)
|
256
|
+
prompt_lower = prompt.downcase
|
257
|
+
|
258
|
+
# Check for different types of common queries
|
259
|
+
return handle_task_creation(prompt, prompt_lower) if task_creation_query?(prompt_lower)
|
260
|
+
return handle_priority_tasks(prompt_lower, "high") if high_priority_query?(prompt_lower)
|
261
|
+
return handle_priority_tasks(prompt_lower, "medium") if medium_priority_query?(prompt_lower)
|
262
|
+
return handle_statistics(prompt_lower) if statistics_query?(prompt_lower)
|
263
|
+
return handle_status_tasks(prompt_lower) if status_tasks_query?(prompt_lower)
|
264
|
+
return handle_notebook_listing(prompt_lower) if notebook_listing_query?(prompt_lower)
|
265
|
+
|
266
|
+
# Not a common query
|
267
|
+
false
|
268
|
+
end
|
269
|
+
|
270
|
+
def task_creation_query?(prompt_lower)
|
271
|
+
(prompt_lower.include?("create") || prompt_lower.include?("add")) &&
|
272
|
+
(prompt_lower.include?("task") || prompt_lower.include?("todo"))
|
273
|
+
end
|
357
274
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
)
|
362
|
-
puts table.render(:ascii)
|
275
|
+
def high_priority_query?(prompt_lower)
|
276
|
+
prompt_lower.include?("high priority") ||
|
277
|
+
(prompt_lower.include?("priority") && prompt_lower.include?("high"))
|
363
278
|
end
|
364
279
|
|
365
|
-
def
|
366
|
-
|
367
|
-
|
368
|
-
|
280
|
+
def medium_priority_query?(prompt_lower)
|
281
|
+
prompt_lower.include?("medium priority") ||
|
282
|
+
(prompt_lower.include?("priority") && prompt_lower.include?("medium"))
|
283
|
+
end
|
284
|
+
|
285
|
+
def statistics_query?(prompt_lower)
|
286
|
+
(prompt_lower.include?("statistics") || prompt_lower.include?("stats")) &&
|
287
|
+
(prompt_lower.include?("notebook") || prompt_lower.include?("tasks"))
|
288
|
+
end
|
289
|
+
|
290
|
+
def status_tasks_query?(prompt_lower)
|
291
|
+
statuses = { "todo" => "todo", "in progress" => "in_progress", "done" => "done", "archived" => "archived" }
|
292
|
+
|
293
|
+
statuses.keys.any? do |name|
|
294
|
+
prompt_lower.include?("#{name} tasks") || prompt_lower.include?("tasks in #{name}")
|
369
295
|
end
|
296
|
+
end
|
370
297
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
298
|
+
def notebook_listing_query?(prompt_lower)
|
299
|
+
prompt_lower.include?("list notebooks") ||
|
300
|
+
prompt_lower.include?("show notebooks") ||
|
301
|
+
prompt_lower.include?("all notebooks")
|
302
|
+
end
|
303
|
+
|
304
|
+
def handle_task_creation(prompt, prompt_lower)
|
305
|
+
say "\n=== Detecting task creation request ===" if options[:verbose]
|
306
|
+
|
307
|
+
title = extract_task_title(prompt)
|
308
|
+
return false unless title
|
376
309
|
|
377
|
-
|
378
|
-
|
379
|
-
|
310
|
+
notebook_name = determine_notebook_name(prompt_lower)
|
311
|
+
return false unless notebook_name
|
312
|
+
|
313
|
+
priority = determine_priority(prompt_lower)
|
314
|
+
|
315
|
+
create_task(notebook_name, title, priority)
|
316
|
+
true
|
317
|
+
end
|
318
|
+
|
319
|
+
def extract_task_title(prompt)
|
320
|
+
# Try to extract title from quotes first
|
321
|
+
title_match = prompt.match(/'([^']+)'|"([^"]+)"/)
|
322
|
+
|
323
|
+
if title_match
|
324
|
+
title_match[1] || title_match[2]
|
325
|
+
else
|
326
|
+
# If no quoted title found, try extracting from the prompt
|
327
|
+
extract_title_from_text(prompt)
|
380
328
|
end
|
329
|
+
end
|
381
330
|
|
382
|
-
|
331
|
+
def extract_title_from_text(prompt)
|
332
|
+
potential_title = prompt
|
333
|
+
phrases_to_remove = [
|
334
|
+
"create a task", "create task", "add a task", "add task",
|
335
|
+
"called", "named", "with", "priority", "high", "medium", "low",
|
336
|
+
"in", "notebook"
|
337
|
+
]
|
383
338
|
|
384
|
-
|
385
|
-
|
386
|
-
return
|
339
|
+
phrases_to_remove.each do |phrase|
|
340
|
+
potential_title = potential_title.gsub(/#{phrase}/i, " ")
|
387
341
|
end
|
388
342
|
|
389
|
-
|
343
|
+
result = potential_title.strip
|
344
|
+
result.empty? ? nil : result
|
390
345
|
end
|
391
346
|
|
392
|
-
def
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
next unless task.title.downcase.include?(query.downcase) ||
|
397
|
-
(task.description && task.description.downcase.include?(query.downcase)) ||
|
398
|
-
(task.tags && task.tags.downcase.include?(query.downcase))
|
347
|
+
def determine_notebook_name(prompt_lower)
|
348
|
+
return nil unless Notebook.default_notebook
|
349
|
+
|
350
|
+
notebook_name = Notebook.default_notebook.name
|
399
351
|
|
400
|
-
|
352
|
+
# Try to extract a specific notebook name from the prompt
|
353
|
+
Notebook.all.each do |notebook|
|
354
|
+
if prompt_lower.include?(notebook.name.downcase)
|
355
|
+
notebook_name = notebook.name
|
356
|
+
break
|
401
357
|
end
|
402
358
|
end
|
403
|
-
|
359
|
+
|
360
|
+
notebook_name
|
361
|
+
end
|
362
|
+
|
363
|
+
def determine_priority(prompt_lower)
|
364
|
+
if prompt_lower.include?("high priority") || prompt_lower.match(/priority.*high/)
|
365
|
+
"high"
|
366
|
+
elsif prompt_lower.include?("medium priority") || prompt_lower.match(/priority.*medium/)
|
367
|
+
"medium"
|
368
|
+
elsif prompt_lower.include?("low priority") || prompt_lower.match(/priority.*low/)
|
369
|
+
"low"
|
370
|
+
end
|
404
371
|
end
|
405
372
|
|
406
|
-
def
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
373
|
+
def create_task(notebook_name, title, priority)
|
374
|
+
say "\nCreating task in notebook: #{notebook_name}" if options[:verbose]
|
375
|
+
cli_args = ["task:add", notebook_name, title]
|
376
|
+
|
377
|
+
# Add priority if specified
|
378
|
+
cli_args.push("--priority", priority) if priority
|
379
|
+
|
380
|
+
RubyTodo::CLI.start(cli_args)
|
381
|
+
|
382
|
+
# Create a simple explanation
|
383
|
+
priority_text = priority ? " with #{priority} priority" : ""
|
384
|
+
say "\nCreated task '#{title}'#{priority_text} in the #{notebook_name} notebook"
|
412
385
|
end
|
413
386
|
|
414
|
-
def
|
415
|
-
|
416
|
-
return nil unless File.exist?(config_file)
|
387
|
+
def handle_priority_tasks(_prompt_lower, priority)
|
388
|
+
say "\n=== Detecting #{priority} priority task request ===" if options[:verbose]
|
417
389
|
|
418
|
-
|
419
|
-
|
390
|
+
return false unless Notebook.default_notebook
|
391
|
+
|
392
|
+
say "\nListing #{priority} priority tasks from default notebook" if options[:verbose]
|
393
|
+
RubyTodo::CLI.start(["task:list", Notebook.default_notebook.name, "--priority", priority])
|
394
|
+
|
395
|
+
# Create a simple explanation
|
396
|
+
say "\nListing all #{priority} priority tasks in the #{Notebook.default_notebook.name} notebook"
|
397
|
+
true
|
420
398
|
end
|
421
399
|
|
422
|
-
def
|
423
|
-
|
424
|
-
|
400
|
+
def handle_statistics(prompt_lower)
|
401
|
+
say "\n=== Detecting statistics request ===" if options[:verbose]
|
402
|
+
|
403
|
+
notebook_name = determine_notebook_name(prompt_lower)
|
425
404
|
|
426
|
-
|
427
|
-
|
428
|
-
"
|
429
|
-
"
|
430
|
-
|
405
|
+
if notebook_name
|
406
|
+
say "\nShowing statistics for notebook: #{notebook_name}" if options[:verbose]
|
407
|
+
RubyTodo::CLI.start(["stats", notebook_name])
|
408
|
+
say "\nDisplaying statistics for the #{notebook_name} notebook"
|
409
|
+
else
|
410
|
+
# Show global stats if no default notebook
|
411
|
+
say "\nShowing global statistics" if options[:verbose]
|
412
|
+
RubyTodo::CLI.start(["stats"])
|
413
|
+
say "\nDisplaying global statistics for all notebooks"
|
414
|
+
end
|
431
415
|
|
432
|
-
|
433
|
-
FileUtils.chmod(0o600, config_file) # Secure the file with private permissions
|
416
|
+
true
|
434
417
|
end
|
435
418
|
|
436
|
-
def
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
419
|
+
def handle_status_tasks(prompt_lower)
|
420
|
+
statuses = { "todo" => "todo", "in progress" => "in_progress", "done" => "done", "archived" => "archived" }
|
421
|
+
|
422
|
+
statuses.each do |name, value|
|
423
|
+
next unless prompt_lower.include?("#{name} tasks") || prompt_lower.include?("tasks in #{name}")
|
424
|
+
|
425
|
+
say "\n=== Detecting #{name} task listing request ===" if options[:verbose]
|
426
|
+
|
427
|
+
return false unless Notebook.default_notebook
|
428
|
+
|
429
|
+
say "\nListing #{name} tasks from default notebook" if options[:verbose]
|
430
|
+
RubyTodo::CLI.start(["task:list", Notebook.default_notebook.name, "--status", value])
|
431
|
+
|
432
|
+
# Create a simple explanation
|
433
|
+
say "\nListing all #{name} tasks in the #{Notebook.default_notebook.name} notebook"
|
434
|
+
return true
|
443
435
|
end
|
436
|
+
|
437
|
+
false
|
444
438
|
end
|
445
439
|
|
446
|
-
def
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
440
|
+
def handle_notebook_listing(_prompt_lower)
|
441
|
+
say "\n=== Detecting notebook listing request ===" if options[:verbose]
|
442
|
+
RubyTodo::CLI.start(["notebook:list"])
|
443
|
+
|
444
|
+
# Create a simple explanation
|
445
|
+
say "\nListing all available notebooks"
|
446
|
+
true
|
451
447
|
end
|
452
448
|
end
|
453
449
|
end
|