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.
@@ -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
- desc "ask [PROMPT]", "Ask the AI assistant to perform tasks using natural language"
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
- unless prompt && !prompt.empty?
16
- say "Please provide a prompt for the AI assistant".red
17
- return
18
- end
26
+ validate_prompt(prompt)
27
+ say "\n=== Starting AI Assistant with prompt: '#{prompt}' ===" if options[:verbose]
19
28
 
20
- # Get API key from options, env var, or config file
21
- api_key = options[:api_key] || ENV["OPENAI_API_KEY"] || load_api_key_from_config
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
- response = query_openai(prompt, context, api_key)
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 build_context
58
- # Build a context object with information about the current state of the app
59
- notebooks = Notebook.all.map do |notebook|
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
- notebooks: notebooks,
73
- commands: {
74
- notebook_commands: %w[create list],
75
- task_commands: %w[add list show edit delete move search],
76
- export_commands: %w[export import],
77
- template_commands: %w[create list show use delete]
78
- },
79
- app_version: RubyTodo::VERSION
80
- }
81
- end
82
-
83
- def query_openai(prompt, context, api_key)
84
- client = OpenAI::Client.new(access_token: api_key)
85
- system_message = build_system_message(context)
86
- make_openai_request(client, system_message, prompt)
87
- end
88
-
89
- def build_system_message(context)
90
- <<~SYSTEM
91
- You are an AI assistant for Ruby Todo, a command-line task management application.
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 create_task(action)
190
- unless action["notebook"] && action["title"]
191
- say "Missing required fields for create_task".red
192
- return
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
- notebook = Notebook.find_by(name: action["notebook"])
196
- unless notebook
197
- say "Notebook '#{action["notebook"]}' not found".red
198
- return
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
- due_date = parse_task_due_date(action["due_date"])
202
- priority = validate_task_priority(action["priority"])
203
-
204
- task = Task.create(
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 parse_task_due_date(due_date_str)
222
- return nil unless due_date_str
102
+ def validate_prompt(prompt)
103
+ return if prompt && !prompt.empty?
223
104
 
224
- begin
225
- Time.parse(due_date_str)
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 validate_task_priority(priority_str)
233
- return nil unless priority_str
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
- if %w[high medium low].include?(priority_str.downcase)
236
- priority_str.downcase
237
- else
238
- msg = "Invalid priority '#{priority_str}'. Valid values are 'high', 'medium', or 'low'."
239
- say "#{msg} Using default.".yellow
240
- nil
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 move_task(action)
245
- unless action["notebook"] && action["task_id"] && action["status"]
246
- say "Missing required fields for move_task".red
247
- return
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
- notebook = Notebook.find_by(name: action["notebook"])
251
- unless notebook
252
- say "Notebook '#{action["notebook"]}' not found".red
253
- return
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
- task = notebook.tasks.find_by(id: action["task_id"])
257
- unless task
258
- say "Task with ID #{action["task_id"]} not found".red
259
- return
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
- valid_statuses = %w[todo in_progress done archived]
263
- status = action["status"].downcase
264
- unless valid_statuses.include?(status)
265
- say "Invalid status '#{status}'. Valid values are: #{valid_statuses.join(", ")}".red
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
- if task.update(status: status)
270
- say "Moved task #{action["task_id"]} to #{status}".green
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
- say "Error moving task: #{task.errors.full_messages.join(", ")}".red
178
+ execute_other_command(cmd_without_prefix)
273
179
  end
274
180
  end
275
181
 
276
- def create_notebook(action)
277
- unless action["name"]
278
- say "Missing required field 'name' for create_notebook".red
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
- notebook = Notebook.create(name: action["name"])
283
- if notebook.valid?
284
- say "Created notebook: #{action["name"]}".green
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 "Error creating notebook: #{notebook.errors.full_messages.join(", ")}".red
191
+ say "\nNo notebook specified for task:list command".yellow
287
192
  end
288
193
  end
289
194
 
290
- def generate_import_json(action)
291
- unless action["notebook"] && action["tasks"] && action["tasks"].is_a?(Array)
292
- say "Missing required fields for generate_import_json".red
293
- return
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
- data = {
297
- "name" => action["notebook"],
298
- "created_at" => Time.now.iso8601,
299
- "updated_at" => Time.now.iso8601,
300
- "tasks" => action["tasks"].map do |task|
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
- if @prompt.yes?("Do you want to import these tasks now?")
317
- import_result = RubyTodo::CLI.new.import(filename)
318
- say "Import complete: #{import_result}"
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 list_tasks(action)
323
- unless action["notebook"]
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
- notebook = Notebook.find_by(name: action["notebook"])
329
- unless notebook
330
- say "Notebook '#{action["notebook"]}' not found".red
331
- return
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
- tasks = notebook.tasks
335
- tasks = tasks.where(status: action["status"]) if action["status"]
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
- if tasks.empty?
339
- say "No tasks found".yellow
340
- return
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
- display_tasks_table(tasks)
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 display_tasks_table(tasks)
347
- rows = tasks.map do |t|
348
- [
349
- t.id,
350
- t.title,
351
- format_status(t.status),
352
- t.priority || "None",
353
- t.due_date ? t.due_date.strftime("%Y-%m-%d %H:%M") : "No due date",
354
- t.tags || "None"
355
- ]
356
- end
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
- table = TTY::Table.new(
359
- header: ["ID", "Title", "Status", "Priority", "Due Date", "Tags"],
360
- rows: rows
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 search_tasks(action)
366
- unless action["query"]
367
- say "Missing required field 'query' for search_tasks".red
368
- return
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
- notebooks = if action["notebook"]
372
- [Notebook.find_by(name: action["notebook"])].compact
373
- else
374
- Notebook.all
375
- end
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
- if notebooks.empty?
378
- say "No notebooks found".yellow
379
- return
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
- results = find_matching_tasks(notebooks, action["query"])
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
- if results.empty?
385
- say "No tasks matching '#{action["query"]}' found".yellow
386
- return
339
+ phrases_to_remove.each do |phrase|
340
+ potential_title = potential_title.gsub(/#{phrase}/i, " ")
387
341
  end
388
342
 
389
- display_search_results(results)
343
+ result = potential_title.strip
344
+ result.empty? ? nil : result
390
345
  end
391
346
 
392
- def find_matching_tasks(notebooks, query)
393
- results = []
394
- notebooks.each do |notebook|
395
- notebook.tasks.each do |task|
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
- results << [notebook.name, task.id, task.title, format_status(task.status)]
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
- results
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 display_search_results(results)
407
- table = TTY::Table.new(
408
- header: %w[Notebook ID Title Status],
409
- rows: results
410
- )
411
- puts table.render(:ascii)
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 load_api_key_from_config
415
- config_file = File.expand_path("~/.ruby_todo/ai_config.json")
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
- config = JSON.parse(File.read(config_file))
419
- config["api_key"]
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 save_config(api, api_key)
423
- config_dir = File.expand_path("~/.ruby_todo")
424
- FileUtils.mkdir_p(config_dir)
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
- config_file = File.join(config_dir, "ai_config.json")
427
- config = {
428
- "api" => api,
429
- "api_key" => api_key
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
- File.write(config_file, JSON.pretty_generate(config))
433
- FileUtils.chmod(0o600, config_file) # Secure the file with private permissions
416
+ true
434
417
  end
435
418
 
436
- def format_status(status)
437
- case status
438
- when "todo" then "Todo".yellow
439
- when "in_progress" then "In Progress".blue
440
- when "done" then "Done".green
441
- when "archived" then "Archived".gray
442
- else status
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 parse_due_date(date_string)
447
- Time.parse(date_string)
448
- rescue ArgumentError
449
- say "Invalid date format. Use YYYY-MM-DD HH:MM format.".red
450
- nil
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