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.
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyTodo
4
+ module OpenAIDocumentation
5
+ CLI_DOCUMENTATION = <<~DOCS
6
+ Available ruby_todo commands:
7
+
8
+ Task Management:
9
+ 1. List tasks:
10
+ ruby_todo task:list [NOTEBOOK]
11
+ - Lists all tasks in a notebook or all notebooks if no name provided
12
+ - To filter by status: ruby_todo task:list [NOTEBOOK] --status STATUS
13
+ - Example: ruby_todo task:list protectors --status in_progress
14
+
15
+ 2. Search tasks:
16
+ ruby_todo task:search SEARCH_TERM
17
+ - Searches for tasks containing the search term
18
+
19
+ 3. Show task details:
20
+ ruby_todo task:show NOTEBOOK TASK_ID
21
+ - Shows detailed information about a specific task
22
+
23
+ 4. Add task:
24
+ ruby_todo task:add [NOTEBOOK] [TITLE]
25
+ - Add a new task to a notebook
26
+ - Interactive prompts for title, description, priority, due date, and tags
27
+
28
+ 5. Edit task:
29
+ ruby_todo task:edit [NOTEBOOK] [TASK_ID]
30
+ - Edit an existing task's details
31
+
32
+ 6. Delete task:
33
+ ruby_todo task:delete [NOTEBOOK] [TASK_ID]
34
+ - Delete a task from a notebook
35
+
36
+ 7. Move task:
37
+ ruby_todo task:move [NOTEBOOK] [TASK_ID] [STATUS]
38
+ - Move a task to a different status
39
+ - STATUS can be: todo, in_progress, done, archived
40
+
41
+ Notebook Management:
42
+ 8. List notebooks:
43
+ ruby_todo notebook:list
44
+ - List all notebooks
45
+
46
+ 9. Create notebook:
47
+ ruby_todo notebook:create NAME
48
+ - Create a new notebook
49
+
50
+ 10. Set default notebook:
51
+ ruby_todo notebook:set_default NAME
52
+ - Set a notebook as the default
53
+
54
+ Template Management:
55
+ 11. List templates:
56
+ ruby_todo template:list
57
+ - List all templates
58
+
59
+ 12. Show template:
60
+ ruby_todo template:show NAME
61
+ - Show details of a specific template
62
+
63
+ 13. Create template:
64
+ ruby_todo template:create NAME --title TITLE
65
+ - Create a new task template
66
+
67
+ 14. Delete template:
68
+ ruby_todo template:delete NAME
69
+ - Delete a template
70
+
71
+ 15. Use template:
72
+ ruby_todo template:use NAME NOTEBOOK
73
+ - Create a task from a template in the specified notebook
74
+
75
+ Other Commands:
76
+ 16. Export tasks:
77
+ ruby_todo export [NOTEBOOK] [FILENAME]
78
+ - Export tasks from a notebook to a JSON file
79
+
80
+ 17. Import tasks:
81
+ ruby_todo import [FILENAME]
82
+ - Import tasks from a JSON or CSV file
83
+
84
+ 18. Show statistics:
85
+ ruby_todo stats [NOTEBOOK]
86
+ - Show statistics for a notebook or all notebooks
87
+
88
+ 19. Initialize:
89
+ ruby_todo init
90
+ - Initialize a new todo list
91
+
92
+ Note: All commands use colons (e.g., 'task:list', 'notebook:list').
93
+ Available statuses: todo, in_progress, done, archived
94
+ DOCS
95
+ end
96
+
97
+ module OpenAIPromptBuilder
98
+ include OpenAIDocumentation
99
+
100
+ private
101
+
102
+ def build_messages(prompt, context)
103
+ messages = [
104
+ { role: "system", content: build_system_prompt(context) },
105
+ { role: "user", content: prompt }
106
+ ]
107
+
108
+ say "\nSystem prompt:\n#{messages.first[:content]}\n" if options[:verbose]
109
+ say "\nUser prompt:\n#{prompt}\n" if options[:verbose]
110
+
111
+ messages
112
+ end
113
+
114
+ def build_system_prompt(context)
115
+ prompt = build_base_prompt
116
+ prompt += build_command_examples
117
+ prompt += build_json_format_requirements
118
+
119
+ if context[:matching_tasks]&.any?
120
+ prompt += build_matching_tasks_info(context)
121
+ end
122
+
123
+ prompt += build_final_instructions
124
+ prompt
125
+ end
126
+
127
+ def build_base_prompt
128
+ prompt = "You are a task management assistant that generates ruby_todo CLI commands. "
129
+ prompt += "Your role is to analyze user requests and generate the appropriate ruby_todo command(s). "
130
+ prompt += "You should respond to ALL types of requests, not just task movement requests."
131
+ prompt += "\n\nHere are the available commands and their usage:\n"
132
+ prompt += CLI_DOCUMENTATION
133
+ prompt += "\nBased on the user's request, generate command(s) that follow these formats exactly."
134
+ prompt += "\n\nStatus Mapping:"
135
+ prompt += "\n- 'pending' maps to 'todo'"
136
+ prompt += "\n- 'in progress' maps to 'in_progress'"
137
+ prompt += "\nPlease use the mapped status values in your commands."
138
+ prompt
139
+ end
140
+
141
+ def build_command_examples
142
+ prompt = "\n\nImportant command formats for common requests:"
143
+ prompt += "\n- To list high priority tasks: ruby_todo task:list [NOTEBOOK] --priority high"
144
+ prompt += "\n- To list tasks with a specific status: ruby_todo task:list [NOTEBOOK] --status [STATUS]"
145
+ prompt += "\n- To list all notebooks: ruby_todo notebook:list"
146
+ prompt += "\n- To create a task: ruby_todo task:add [NOTEBOOK] [TITLE]"
147
+ prompt += "\n- To move a task to a status: ruby_todo task:move [NOTEBOOK] [TASK_ID] [STATUS]"
148
+
149
+ prompt += "\n\nExamples of specific requests and corresponding commands:"
150
+ prompt += "\n- 'show me all high priority tasks' → ruby_todo task:list protectors --priority high"
151
+ prompt += "\n- 'list tasks that are in progress' → ruby_todo task:list protectors --status in_progress"
152
+ prompt += "\n- 'show me all notebooks' → ruby_todo notebook:list"
153
+ prompt += "\n- 'move task 5 to done' → ruby_todo task:move protectors 5 done"
154
+ prompt
155
+ end
156
+
157
+ def build_json_format_requirements
158
+ prompt = "\n\nYou MUST respond with a JSON object containing:"
159
+ prompt += "\n- 'commands': an array of commands to execute"
160
+ prompt += "\n- 'explanation': a brief explanation of what the commands will do"
161
+ prompt
162
+ end
163
+
164
+ def build_matching_tasks_info(context)
165
+ prompt = "\n\nRelevant tasks found in the system:\n"
166
+ context[:matching_tasks].each do |task|
167
+ task_info = format_task_info(task)
168
+ prompt += "- #{task_info}\n"
169
+ end
170
+
171
+ if context[:target_status] && context[:search_term]
172
+ # Break this into multiple lines to avoid exceeding line length
173
+ prompt += "\n\nI will move these tasks matching "
174
+ prompt += "'#{context[:search_term]}' to "
175
+ prompt += "'#{context[:target_status]}' status."
176
+ end
177
+ prompt
178
+ end
179
+
180
+ def build_final_instructions
181
+ prompt = "\n\nEven if no tasks match a search or if your request isn't about task movement, "
182
+ prompt += "I still need you to return a JSON response with commands and explanation."
183
+ prompt += "\n\nExample JSON Response:"
184
+ prompt += "\n```json"
185
+ prompt += "\n{"
186
+ prompt += "\n \"commands\": ["
187
+ prompt += "\n \"ruby_todo task:list protectors\","
188
+ prompt += "\n \"ruby_todo stats protectors\""
189
+ prompt += "\n ],"
190
+ prompt += "\n \"explanation\": \"Listing all tasks and statistics for the protectors notebook\""
191
+ prompt += "\n}"
192
+ prompt += "\n```"
193
+
194
+ prompt += "\n\nNote that all commands use colons, not underscores (e.g., 'task:list', not 'task_list')."
195
+ prompt
196
+ end
197
+
198
+ def format_task_info(task)
199
+ "Task #{task[:task_id]} in notebook '#{task[:notebook]}': " \
200
+ "#{task[:title]} (Status: #{task[:status]})"
201
+ end
202
+ end
203
+
204
+ module OpenAIIntegration
205
+ include OpenAIDocumentation
206
+ include OpenAIPromptBuilder
207
+
208
+ def query_openai(prompt, context, api_key)
209
+ say "\nMaking OpenAI API call...".blue if options[:verbose]
210
+ client = OpenAI::Client.new(access_token: api_key)
211
+ messages = build_messages(prompt, context)
212
+ say "Sending request to OpenAI..." if options[:verbose]
213
+
214
+ response = client.chat(parameters: build_openai_parameters(messages))
215
+
216
+ say "\nOpenAI API call completed".green if options[:verbose]
217
+
218
+ log_raw_response(response) if options[:verbose]
219
+
220
+ parsed_response = handle_openai_response(response)
221
+
222
+ log_parsed_response(parsed_response) if options[:verbose] && parsed_response
223
+
224
+ parsed_response
225
+ end
226
+
227
+ private
228
+
229
+ def build_openai_parameters(messages)
230
+ {
231
+ model: "gpt-4o-mini",
232
+ messages: messages,
233
+ temperature: 0.7,
234
+ max_tokens: 500
235
+ }
236
+ end
237
+
238
+ def log_raw_response(response)
239
+ say "\n=== RAW OPENAI RESPONSE ==="
240
+ if response && response.dig("choices", 0, "message", "content")
241
+ say response["choices"][0]["message"]["content"]
242
+ else
243
+ say "No content in response"
244
+ end
245
+ say "=== END RAW RESPONSE ===\n"
246
+ end
247
+
248
+ def log_parsed_response(parsed_response)
249
+ say "\n=== PARSED RESPONSE DETAILS ==="
250
+ say "Commands array type: #{parsed_response["commands"].class}"
251
+ say "Number of commands: #{parsed_response["commands"].size}"
252
+ parsed_response["commands"].each_with_index do |cmd, i|
253
+ say "Command #{i + 1}: '#{cmd}'"
254
+ end
255
+ say "=== END RESPONSE DETAILS ===\n"
256
+ end
257
+
258
+ def handle_openai_response(response)
259
+ return nil unless response&.dig("choices", 0, "message", "content")
260
+
261
+ content = response["choices"][0]["message"]["content"]
262
+ say "\nAI Response:\n#{content}\n" if options[:verbose]
263
+
264
+ parse_json_from_content(content)
265
+ end
266
+
267
+ def parse_json_from_content(content)
268
+ # Process the content to extract JSON
269
+ json_content = process_json_content(content)
270
+
271
+ say "\nProcessed JSON content:\n#{json_content}\n" if options[:verbose]
272
+
273
+ # Parse the JSON
274
+ result = JSON.parse(json_content)
275
+
276
+ # Ensure required keys exist
277
+ validate_and_fix_json_result(result)
278
+
279
+ result
280
+ rescue JSON::ParserError => e
281
+ handle_json_parse_error(content, e)
282
+ end
283
+
284
+ def process_json_content(content)
285
+ # Remove markdown formatting if present
286
+ json_content = content.gsub(/```(?:json)?\n(.*?)\n```/m, '\1')
287
+ # Strip any leading/trailing whitespace, braces are required
288
+ json_content = json_content.strip
289
+ # Add braces if they're missing
290
+ json_content = "{#{json_content}}" unless json_content.start_with?("{") && json_content.end_with?("}")
291
+
292
+ json_content
293
+ end
294
+
295
+ def validate_and_fix_json_result(result)
296
+ # Ensure we have the required keys
297
+ if !result.key?("commands") || !result["commands"].is_a?(Array)
298
+ say "Warning: AI response missing 'commands' array. Adding empty array.".yellow if options[:verbose]
299
+ result["commands"] = []
300
+ end
301
+
302
+ if !result.key?("explanation") || !result["explanation"].is_a?(String)
303
+ say "Warning: AI response missing 'explanation'. Adding default.".yellow if options[:verbose]
304
+ result["explanation"] = "Command execution completed."
305
+ end
306
+ end
307
+
308
+ def handle_json_parse_error(content, error)
309
+ say "Error parsing AI response: #{error.message}".red if options[:verbose]
310
+
311
+ # Try to extract commands from plain text as fallback
312
+ commands = extract_commands_from_text(content)
313
+
314
+ if commands.any?
315
+ say "Extracted #{commands.size} commands from text response".yellow if options[:verbose]
316
+ return {
317
+ "commands" => commands,
318
+ "explanation" => "Commands extracted from non-JSON response."
319
+ }
320
+ end
321
+
322
+ nil
323
+ end
324
+
325
+ def extract_commands_from_text(content)
326
+ commands = []
327
+ content.scan(/ruby_todo\s+\S+(?:\s+\S+)*/) do |cmd|
328
+ commands << cmd.strip
329
+ end
330
+ commands
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyTodo
4
+ module TaskCreation
5
+ private
6
+
7
+ def task_creation_query?(prompt_lower)
8
+ (prompt_lower.include?("create") || prompt_lower.include?("add")) &&
9
+ (prompt_lower.include?("task") || prompt_lower.include?("todo"))
10
+ end
11
+
12
+ def handle_task_creation(prompt, prompt_lower)
13
+ say "\n=== Detecting task creation request ===" if options[:verbose]
14
+
15
+ title = extract_task_title(prompt)
16
+ return false unless title
17
+
18
+ notebook_name = determine_notebook_name(prompt_lower)
19
+ return false unless notebook_name
20
+
21
+ priority = determine_priority(prompt_lower)
22
+ create_task(notebook_name, title, priority)
23
+
24
+ true
25
+ end
26
+
27
+ def extract_task_title(prompt)
28
+ # Try to extract title from quotes first
29
+ title_match = prompt.match(/'([^']+)'|"([^"]+)"/)
30
+ if title_match
31
+ title_match[1] || title_match[2]
32
+ else
33
+ extract_title_from_text(prompt)
34
+ end
35
+ end
36
+
37
+ def extract_title_from_text(prompt)
38
+ potential_title = prompt
39
+ .gsub(/(?:create|add)\s+(?:a\s+)?(?:new\s+)?(?:task|todo)/i, "")
40
+ .gsub(/(?:in|to)\s+(?:the\s+)?notebook/i, "")
41
+ .gsub(/(?:with|as)\s+(?:high|medium|low)\s+priority/i, "")
42
+ .strip
43
+
44
+ return nil if potential_title.empty?
45
+
46
+ potential_title
47
+ end
48
+
49
+ def determine_notebook_name(prompt_lower)
50
+ return nil unless Notebook.default_notebook
51
+
52
+ if prompt_lower =~ /(?:in|to)\s+(?:the\s+)?notebook\s+['"]?([^'"]+)['"]?/i
53
+ notebook_name = Regexp.last_match(1).strip
54
+ return notebook_name if Notebook.exists?(notebook_name)
55
+ end
56
+
57
+ Notebook.default_notebook.name
58
+ end
59
+
60
+ def determine_priority(prompt_lower)
61
+ if prompt_lower.include?("high priority") || prompt_lower.match(/priority.*high/)
62
+ "high"
63
+ elsif prompt_lower.include?("medium priority") || prompt_lower.match(/priority.*medium/)
64
+ "medium"
65
+ elsif prompt_lower.include?("low priority") || prompt_lower.match(/priority.*low/)
66
+ "low"
67
+ end
68
+ end
69
+
70
+ def create_task(notebook_name, title, priority)
71
+ say "\nCreating task in notebook: #{notebook_name}" if options[:verbose]
72
+
73
+ notebook = Notebook.find_by_name(notebook_name)
74
+ task = notebook.create_task(title: title)
75
+
76
+ if priority
77
+ task.update(tags: priority)
78
+ say "Created task with #{priority} priority: #{title}".green
79
+ else
80
+ say "Created task: #{title}".green
81
+ end
82
+
83
+ task
84
+ end
85
+ end
86
+ end