ruby_todo 0.4.1 → 1.0.0
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/.env.template +2 -0
- data/CHANGELOG.md +5 -0
- data/README.md +37 -73
- data/ai_assistant_implementation.md +611 -0
- data/delete_notebooks.rb +20 -0
- data/implementation_steps.md +130 -0
- data/lib/ruby_todo/cli.rb +5 -0
- data/lib/ruby_todo/commands/ai_assistant.rb +453 -0
- data/lib/ruby_todo/version.rb +1 -1
- metadata +34 -1
@@ -0,0 +1,611 @@
|
|
1
|
+
# AI Assistant Integration for Ruby Todo
|
2
|
+
|
3
|
+
This document outlines the implementation plan for adding AI assistant functionality to the Ruby Todo gem, enabling users to interact with the application using natural language through Claude or OpenAI's APIs.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The AI assistant will allow users to:
|
8
|
+
1. Create tasks using natural language
|
9
|
+
2. Move tasks between statuses
|
10
|
+
3. Create and import task JSON files
|
11
|
+
4. Get summaries of tasks and notebooks
|
12
|
+
5. Ask questions about their tasks
|
13
|
+
6. Perform bulk operations with simple instructions
|
14
|
+
|
15
|
+
## Implementation Plan
|
16
|
+
|
17
|
+
### 1. Add Dependencies
|
18
|
+
|
19
|
+
Add the following gems to the gemspec:
|
20
|
+
```ruby
|
21
|
+
spec.add_dependency "anthropic", "~> 0.1.0" # For Claude API
|
22
|
+
spec.add_dependency "ruby-openai", "~> 6.3.0" # For OpenAI API
|
23
|
+
spec.add_dependency "dotenv", "~> 2.8" # For API key management
|
24
|
+
```
|
25
|
+
|
26
|
+
### 2. Create AI Assistant Command Class
|
27
|
+
|
28
|
+
Create a new class in `lib/ruby_todo/commands/ai_assistant.rb` to handle AI-related commands:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
# frozen_string_literal: true
|
32
|
+
|
33
|
+
require "thor"
|
34
|
+
require "json"
|
35
|
+
require "anthropic"
|
36
|
+
require "openai"
|
37
|
+
require "dotenv/load"
|
38
|
+
|
39
|
+
module RubyTodo
|
40
|
+
class AIAssistantCommand < Thor
|
41
|
+
desc "ask [PROMPT]", "Ask the AI assistant to perform tasks using natural language"
|
42
|
+
method_option :api, type: :string, default: "claude", desc: "API to use (claude or openai)"
|
43
|
+
method_option :api_key, type: :string, desc: "API key for the selected service"
|
44
|
+
method_option :model, type: :string, desc: "Model to use (claude-3-opus-20240229, gpt-4, etc.)"
|
45
|
+
method_option :verbose, type: :boolean, default: false, desc: "Show detailed response"
|
46
|
+
def ask(*prompt_args)
|
47
|
+
prompt = prompt_args.join(" ")
|
48
|
+
unless prompt && !prompt.empty?
|
49
|
+
say "Please provide a prompt for the AI assistant".red
|
50
|
+
return
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get API key from options, env var, or config file
|
54
|
+
api_key = options[:api_key] || ENV["ANTHROPIC_API_KEY"] || ENV["OPENAI_API_KEY"] || load_api_key_from_config
|
55
|
+
unless api_key
|
56
|
+
say "No API key found. Please provide an API key using --api-key or set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable".red
|
57
|
+
return
|
58
|
+
end
|
59
|
+
|
60
|
+
# Set up context for the AI
|
61
|
+
context = build_context
|
62
|
+
|
63
|
+
response = if options[:api] == "openai"
|
64
|
+
query_openai(prompt, context, api_key, options[:model] || "gpt-4")
|
65
|
+
else
|
66
|
+
query_claude(prompt, context, api_key, options[:model] || "claude-3-opus-20240229")
|
67
|
+
end
|
68
|
+
|
69
|
+
if response[:error]
|
70
|
+
say "Error: #{response[:error]}".red
|
71
|
+
return
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parse the AI's response for potential actions
|
75
|
+
parse_and_execute_actions(response[:content])
|
76
|
+
|
77
|
+
# Print the AI's response if verbose mode
|
78
|
+
if options[:verbose]
|
79
|
+
say "\nAI Assistant Response:".blue
|
80
|
+
say response[:content]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
desc "configure", "Configure the AI assistant settings"
|
85
|
+
def configure
|
86
|
+
prompt = TTY::Prompt.new
|
87
|
+
api = prompt.select("Which AI provider would you like to use?", %w[Claude OpenAI])
|
88
|
+
|
89
|
+
api_key = prompt.mask("Enter your API key:")
|
90
|
+
|
91
|
+
model = "gpt-4o-mino"
|
92
|
+
|
93
|
+
save_config(api.downcase, api_key, model)
|
94
|
+
say "Configuration saved successfully!".green
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def build_context
|
100
|
+
# Build a context object with information about the current state of the app
|
101
|
+
notebooks = Notebook.all.map do |notebook|
|
102
|
+
{
|
103
|
+
id: notebook.id,
|
104
|
+
name: notebook.name,
|
105
|
+
task_count: notebook.tasks.count,
|
106
|
+
todo_count: notebook.tasks.todo.count,
|
107
|
+
in_progress_count: notebook.tasks.in_progress.count,
|
108
|
+
done_count: notebook.tasks.done.count,
|
109
|
+
archived_count: notebook.tasks.archived.count
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
{
|
114
|
+
notebooks: notebooks,
|
115
|
+
commands: {
|
116
|
+
notebook_commands: ["create", "list"],
|
117
|
+
task_commands: ["add", "list", "show", "edit", "delete", "move", "search"],
|
118
|
+
export_commands: ["export", "import"],
|
119
|
+
template_commands: ["create", "list", "show", "use", "delete"]
|
120
|
+
},
|
121
|
+
app_version: RubyTodo::VERSION
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def query_claude(prompt, context, api_key, model)
|
126
|
+
client = Anthropic::Client.new(api_key: api_key)
|
127
|
+
|
128
|
+
# Prepare system message with context
|
129
|
+
system_message = <<~SYSTEM
|
130
|
+
You are an AI assistant for Ruby Todo, a command-line task management application.
|
131
|
+
|
132
|
+
Application Information:
|
133
|
+
#{JSON.pretty_generate(context)}
|
134
|
+
|
135
|
+
Your job is to help the user manage their tasks through natural language.
|
136
|
+
You can create tasks, move tasks between statuses, and more.
|
137
|
+
|
138
|
+
When the user asks you to perform an action, generate a response in this JSON format:
|
139
|
+
{
|
140
|
+
"explanation": "Brief explanation of what you're doing",
|
141
|
+
"actions": [
|
142
|
+
{"type": "create_task", "notebook": "Work", "title": "Task title", "description": "Task description", "priority": "high", "tags": "tag1,tag2", "due_date": "2024-04-10 14:00"},
|
143
|
+
{"type": "move_task", "notebook": "Work", "task_id": 1, "status": "in_progress"},
|
144
|
+
{"type": "create_notebook", "name": "Personal"},
|
145
|
+
{"type": "generate_import_json", "notebook": "Work", "tasks": [{...task data...}]}
|
146
|
+
]
|
147
|
+
}
|
148
|
+
|
149
|
+
Action types:
|
150
|
+
- create_task: Create a new task in a notebook
|
151
|
+
- move_task: Move a task to a different status
|
152
|
+
- create_notebook: Create a new notebook
|
153
|
+
- generate_import_json: Generate JSON for importing tasks
|
154
|
+
- list_tasks: List tasks in a notebook
|
155
|
+
- search_tasks: Search for tasks
|
156
|
+
|
157
|
+
Always respond in valid JSON format.
|
158
|
+
SYSTEM
|
159
|
+
|
160
|
+
begin
|
161
|
+
response = client.messages.create(
|
162
|
+
model: model,
|
163
|
+
system: system_message,
|
164
|
+
messages: [
|
165
|
+
{ role: "user", content: prompt }
|
166
|
+
],
|
167
|
+
max_tokens: 1024
|
168
|
+
)
|
169
|
+
|
170
|
+
return { content: response.content.first.text }
|
171
|
+
rescue => e
|
172
|
+
return { error: e.message }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def query_openai(prompt, context, api_key, model)
|
177
|
+
client = OpenAI::Client.new(access_token: api_key)
|
178
|
+
|
179
|
+
# Prepare system message with context
|
180
|
+
system_message = <<~SYSTEM
|
181
|
+
You are an AI assistant for Ruby Todo, a command-line task management application.
|
182
|
+
|
183
|
+
Application Information:
|
184
|
+
#{JSON.pretty_generate(context)}
|
185
|
+
|
186
|
+
Your job is to help the user manage their tasks through natural language.
|
187
|
+
You can create tasks, move tasks between statuses, and more.
|
188
|
+
|
189
|
+
When the user asks you to perform an action, generate a response in this JSON format:
|
190
|
+
{
|
191
|
+
"explanation": "Brief explanation of what you're doing",
|
192
|
+
"actions": [
|
193
|
+
{"type": "create_task", "notebook": "Work", "title": "Task title", "description": "Task description", "priority": "high", "tags": "tag1,tag2", "due_date": "2024-04-10 14:00"},
|
194
|
+
{"type": "move_task", "notebook": "Work", "task_id": 1, "status": "in_progress"},
|
195
|
+
{"type": "create_notebook", "name": "Personal"},
|
196
|
+
{"type": "generate_import_json", "notebook": "Work", "tasks": [{...task data...}]}
|
197
|
+
]
|
198
|
+
}
|
199
|
+
|
200
|
+
Action types:
|
201
|
+
- create_task: Create a new task in a notebook
|
202
|
+
- move_task: Move a task to a different status
|
203
|
+
- create_notebook: Create a new notebook
|
204
|
+
- generate_import_json: Generate JSON for importing tasks
|
205
|
+
- list_tasks: List tasks in a notebook
|
206
|
+
- search_tasks: Search for tasks
|
207
|
+
|
208
|
+
Always respond in valid JSON format.
|
209
|
+
SYSTEM
|
210
|
+
|
211
|
+
begin
|
212
|
+
response = client.chat(
|
213
|
+
parameters: {
|
214
|
+
model: model,
|
215
|
+
messages: [
|
216
|
+
{ role: "system", content: system_message },
|
217
|
+
{ role: "user", content: prompt }
|
218
|
+
],
|
219
|
+
max_tokens: 1024
|
220
|
+
}
|
221
|
+
)
|
222
|
+
|
223
|
+
return { content: response.dig("choices", 0, "message", "content") }
|
224
|
+
rescue => e
|
225
|
+
return { error: e.message }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def parse_and_execute_actions(response)
|
230
|
+
begin
|
231
|
+
# Parse the JSON response
|
232
|
+
data = JSON.parse(response)
|
233
|
+
|
234
|
+
# Print explanation
|
235
|
+
say data["explanation"].green if data["explanation"]
|
236
|
+
|
237
|
+
# Execute each action
|
238
|
+
if data["actions"] && data["actions"].is_a?(Array)
|
239
|
+
data["actions"].each do |action|
|
240
|
+
execute_action(action)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
rescue JSON::ParserError => e
|
244
|
+
say "Couldn't parse AI response: #{e.message}".red
|
245
|
+
say "Raw response: #{response}" if options[:verbose]
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def execute_action(action)
|
250
|
+
case action["type"]
|
251
|
+
when "create_task"
|
252
|
+
create_task(action)
|
253
|
+
when "move_task"
|
254
|
+
move_task(action)
|
255
|
+
when "create_notebook"
|
256
|
+
create_notebook(action)
|
257
|
+
when "generate_import_json"
|
258
|
+
generate_import_json(action)
|
259
|
+
when "list_tasks"
|
260
|
+
list_tasks(action)
|
261
|
+
when "search_tasks"
|
262
|
+
search_tasks(action)
|
263
|
+
else
|
264
|
+
say "Unknown action type: #{action["type"]}".yellow
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def create_task(action)
|
269
|
+
# Validate required fields
|
270
|
+
unless action["notebook"] && action["title"]
|
271
|
+
say "Missing required fields for create_task".red
|
272
|
+
return
|
273
|
+
end
|
274
|
+
|
275
|
+
# Find notebook
|
276
|
+
notebook = Notebook.find_by(name: action["notebook"])
|
277
|
+
unless notebook
|
278
|
+
say "Notebook '#{action["notebook"]}' not found".red
|
279
|
+
return
|
280
|
+
end
|
281
|
+
|
282
|
+
# Parse due date if present
|
283
|
+
due_date = action["due_date"] ? parse_due_date(action["due_date"]) : nil
|
284
|
+
|
285
|
+
# Create task
|
286
|
+
task = Task.create(
|
287
|
+
notebook: notebook,
|
288
|
+
title: action["title"],
|
289
|
+
description: action["description"],
|
290
|
+
due_date: due_date,
|
291
|
+
priority: action["priority"],
|
292
|
+
tags: action["tags"],
|
293
|
+
status: "todo"
|
294
|
+
)
|
295
|
+
|
296
|
+
if task.valid?
|
297
|
+
say "Added task: #{action["title"]}".green
|
298
|
+
else
|
299
|
+
say "Error creating task: #{task.errors.full_messages.join(", ")}".red
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def move_task(action)
|
304
|
+
# Validate required fields
|
305
|
+
unless action["notebook"] && action["task_id"] && action["status"]
|
306
|
+
say "Missing required fields for move_task".red
|
307
|
+
return
|
308
|
+
end
|
309
|
+
|
310
|
+
# Find notebook
|
311
|
+
notebook = Notebook.find_by(name: action["notebook"])
|
312
|
+
unless notebook
|
313
|
+
say "Notebook '#{action["notebook"]}' not found".red
|
314
|
+
return
|
315
|
+
end
|
316
|
+
|
317
|
+
# Find task
|
318
|
+
task = notebook.tasks.find_by(id: action["task_id"])
|
319
|
+
unless task
|
320
|
+
say "Task with ID #{action["task_id"]} not found".red
|
321
|
+
return
|
322
|
+
end
|
323
|
+
|
324
|
+
# Update task status
|
325
|
+
if task.update(status: action["status"])
|
326
|
+
say "Moved task #{action["task_id"]} to #{action["status"]}".green
|
327
|
+
else
|
328
|
+
say "Error moving task: #{task.errors.full_messages.join(", ")}".red
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def create_notebook(action)
|
333
|
+
# Validate required fields
|
334
|
+
unless action["name"]
|
335
|
+
say "Missing required field 'name' for create_notebook".red
|
336
|
+
return
|
337
|
+
end
|
338
|
+
|
339
|
+
# Create notebook
|
340
|
+
notebook = Notebook.create(name: action["name"])
|
341
|
+
if notebook.valid?
|
342
|
+
say "Created notebook: #{action["name"]}".green
|
343
|
+
else
|
344
|
+
say "Error creating notebook: #{notebook.errors.full_messages.join(", ")}".red
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def generate_import_json(action)
|
349
|
+
# Validate required fields
|
350
|
+
unless action["notebook"] && action["tasks"] && action["tasks"].is_a?(Array)
|
351
|
+
say "Missing required fields for generate_import_json".red
|
352
|
+
return
|
353
|
+
end
|
354
|
+
|
355
|
+
# Create JSON structure
|
356
|
+
data = {
|
357
|
+
"name" => action["notebook"],
|
358
|
+
"created_at" => Time.now.iso8601,
|
359
|
+
"updated_at" => Time.now.iso8601,
|
360
|
+
"tasks" => action["tasks"].map do |task|
|
361
|
+
{
|
362
|
+
"title" => task["title"],
|
363
|
+
"description" => task["description"],
|
364
|
+
"status" => task["status"] || "todo",
|
365
|
+
"priority" => task["priority"],
|
366
|
+
"tags" => task["tags"],
|
367
|
+
"due_date" => task["due_date"]
|
368
|
+
}
|
369
|
+
end
|
370
|
+
}
|
371
|
+
|
372
|
+
# Write JSON to file
|
373
|
+
filename = action["filename"] || "#{action["notebook"].downcase.gsub(/\s+/, '_')}_tasks.json"
|
374
|
+
File.write(filename, JSON.pretty_generate(data))
|
375
|
+
say "Generated import JSON file: #{filename}".green
|
376
|
+
|
377
|
+
# Offer to import the file
|
378
|
+
if @prompt.yes?("Do you want to import these tasks now?")
|
379
|
+
import_result = RubyTodo::CLI.new.import(filename)
|
380
|
+
say "Import complete: #{import_result}"
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def list_tasks(action)
|
385
|
+
# Validate required fields
|
386
|
+
unless action["notebook"]
|
387
|
+
say "Missing required field 'notebook' for list_tasks".red
|
388
|
+
return
|
389
|
+
end
|
390
|
+
|
391
|
+
# Find notebook
|
392
|
+
notebook = Notebook.find_by(name: action["notebook"])
|
393
|
+
unless notebook
|
394
|
+
say "Notebook '#{action["notebook"]}' not found".red
|
395
|
+
return
|
396
|
+
end
|
397
|
+
|
398
|
+
# Apply filters
|
399
|
+
tasks = notebook.tasks
|
400
|
+
tasks = tasks.where(status: action["status"]) if action["status"]
|
401
|
+
tasks = tasks.where(priority: action["priority"]) if action["priority"]
|
402
|
+
|
403
|
+
# Display tasks
|
404
|
+
if tasks.empty?
|
405
|
+
say "No tasks found".yellow
|
406
|
+
return
|
407
|
+
end
|
408
|
+
|
409
|
+
rows = tasks.map do |t|
|
410
|
+
[
|
411
|
+
t.id,
|
412
|
+
t.title,
|
413
|
+
format_status(t.status),
|
414
|
+
t.priority || "None",
|
415
|
+
t.due_date ? t.due_date.strftime("%Y-%m-%d %H:%M") : "No due date",
|
416
|
+
t.tags || "None"
|
417
|
+
]
|
418
|
+
end
|
419
|
+
|
420
|
+
table = TTY::Table.new(
|
421
|
+
header: ["ID", "Title", "Status", "Priority", "Due Date", "Tags"],
|
422
|
+
rows: rows
|
423
|
+
)
|
424
|
+
puts table.render(:ascii)
|
425
|
+
end
|
426
|
+
|
427
|
+
def search_tasks(action)
|
428
|
+
# Validate required fields
|
429
|
+
unless action["query"]
|
430
|
+
say "Missing required field 'query' for search_tasks".red
|
431
|
+
return
|
432
|
+
end
|
433
|
+
|
434
|
+
# Determine notebooks
|
435
|
+
notebooks = if action["notebook"]
|
436
|
+
[Notebook.find_by(name: action["notebook"])].compact
|
437
|
+
else
|
438
|
+
Notebook.all
|
439
|
+
end
|
440
|
+
|
441
|
+
if notebooks.empty?
|
442
|
+
say "No notebooks found".yellow
|
443
|
+
return
|
444
|
+
end
|
445
|
+
|
446
|
+
# Search for tasks
|
447
|
+
results = []
|
448
|
+
notebooks.each do |notebook|
|
449
|
+
notebook.tasks.each do |task|
|
450
|
+
next unless task.title.downcase.include?(action["query"].downcase) ||
|
451
|
+
(task.description && task.description.downcase.include?(action["query"].downcase)) ||
|
452
|
+
(task.tags && task.tags.downcase.include?(action["query"].downcase))
|
453
|
+
|
454
|
+
results << [notebook.name, task.id, task.title, format_status(task.status)]
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
if results.empty?
|
459
|
+
say "No tasks matching '#{action["query"]}' found".yellow
|
460
|
+
return
|
461
|
+
end
|
462
|
+
|
463
|
+
table = TTY::Table.new(
|
464
|
+
header: %w[Notebook ID Title Status],
|
465
|
+
rows: results
|
466
|
+
)
|
467
|
+
puts table.render(:ascii)
|
468
|
+
end
|
469
|
+
|
470
|
+
def load_api_key_from_config
|
471
|
+
config_file = File.expand_path("~/.ruby_todo/ai_config.json")
|
472
|
+
return nil unless File.exist?(config_file)
|
473
|
+
|
474
|
+
config = JSON.parse(File.read(config_file))
|
475
|
+
config["api_key"]
|
476
|
+
end
|
477
|
+
|
478
|
+
def save_config(api, api_key, model)
|
479
|
+
config_dir = File.expand_path("~/.ruby_todo")
|
480
|
+
FileUtils.mkdir_p(config_dir)
|
481
|
+
|
482
|
+
config_file = File.join(config_dir, "ai_config.json")
|
483
|
+
config = {
|
484
|
+
"api" => api,
|
485
|
+
"api_key" => api_key,
|
486
|
+
"model" => model
|
487
|
+
}
|
488
|
+
|
489
|
+
File.write(config_file, JSON.pretty_generate(config))
|
490
|
+
FileUtils.chmod(0600, config_file) # Secure the file with private permissions
|
491
|
+
end
|
492
|
+
|
493
|
+
# Helper methods for formatting
|
494
|
+
def format_status(status)
|
495
|
+
case status
|
496
|
+
when "todo" then "Todo".yellow
|
497
|
+
when "in_progress" then "In Progress".blue
|
498
|
+
when "done" then "Done".green
|
499
|
+
when "archived" then "Archived".gray
|
500
|
+
else status
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
def parse_due_date(date_string)
|
505
|
+
Time.parse(date_string)
|
506
|
+
rescue ArgumentError
|
507
|
+
say "Invalid date format. Use YYYY-MM-DD HH:MM format.".red
|
508
|
+
nil
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
```
|
513
|
+
|
514
|
+
### 3. Integrate AI Assistant Command with CLI
|
515
|
+
|
516
|
+
Update `lib/ruby_todo/cli.rb` to register the AI Assistant command:
|
517
|
+
|
518
|
+
```ruby
|
519
|
+
# Add the require statement at the top
|
520
|
+
require_relative "commands/ai_assistant"
|
521
|
+
|
522
|
+
module RubyTodo
|
523
|
+
class CLI < Thor
|
524
|
+
# Existing code...
|
525
|
+
|
526
|
+
# Register AI Assistant subcommand
|
527
|
+
desc "ai SUBCOMMAND", "Use AI assistant"
|
528
|
+
subcommand "ai", AIAssistantCommand
|
529
|
+
|
530
|
+
# More existing code...
|
531
|
+
end
|
532
|
+
end
|
533
|
+
```
|
534
|
+
|
535
|
+
### 4. Add Configuration Files
|
536
|
+
|
537
|
+
Create sample `.env` file for API key management:
|
538
|
+
|
539
|
+
```
|
540
|
+
ANTHROPIC_API_KEY=your_claude_api_key_here
|
541
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
542
|
+
```
|
543
|
+
|
544
|
+
### 5. Update README Documentation
|
545
|
+
|
546
|
+
Add new section to README.md for AI Assistant usage:
|
547
|
+
|
548
|
+
```markdown
|
549
|
+
### AI Assistant
|
550
|
+
|
551
|
+
Ruby Todo includes an AI assistant powered by Claude or OpenAI that can help you manage your tasks using natural language.
|
552
|
+
|
553
|
+
#### Configuration
|
554
|
+
|
555
|
+
Configure your AI assistant:
|
556
|
+
```bash
|
557
|
+
$ ruby_todo ai configure
|
558
|
+
```
|
559
|
+
|
560
|
+
#### Using the AI Assistant
|
561
|
+
|
562
|
+
Ask the AI assistant to perform actions:
|
563
|
+
```bash
|
564
|
+
$ ruby_todo ai ask "Create a new task in my Work notebook to update the documentation by next Friday"
|
565
|
+
```
|
566
|
+
|
567
|
+
```bash
|
568
|
+
$ ruby_todo ai ask "Move all tasks related to the API project to in_progress status"
|
569
|
+
```
|
570
|
+
|
571
|
+
```bash
|
572
|
+
$ ruby_todo ai ask "Create a JSON to import 5 new tasks for my upcoming vacation"
|
573
|
+
```
|
574
|
+
|
575
|
+
Pass in an API key directly (if not configured):
|
576
|
+
```bash
|
577
|
+
$ ruby_todo ai ask "What tasks are overdue?" --api-key=your_api_key_here --api=claude
|
578
|
+
```
|
579
|
+
|
580
|
+
Enable verbose mode to see full AI responses:
|
581
|
+
```bash
|
582
|
+
$ ruby_todo ai ask "Summarize my Work notebook" --verbose
|
583
|
+
```
|
584
|
+
```
|
585
|
+
|
586
|
+
## Implementation Timeline
|
587
|
+
|
588
|
+
1. Add new dependencies to gemspec
|
589
|
+
2. Create the AI Assistant command structure
|
590
|
+
3. Implement API integration (Claude and OpenAI)
|
591
|
+
4. Add action parsing and execution
|
592
|
+
5. Create configuration management
|
593
|
+
6. Update documentation and examples
|
594
|
+
7. Test with various prompts
|
595
|
+
|
596
|
+
## Security Considerations
|
597
|
+
|
598
|
+
1. API keys stored in `~/.ruby_todo/ai_config.json` should have appropriate file permissions (0600)
|
599
|
+
2. Environmental variables can be used instead of configuration files
|
600
|
+
3. Options for passing API keys directly in CLI should warn about shell history
|
601
|
+
4. Input validation to prevent command injection
|
602
|
+
5. Error handling to prevent exposing sensitive information
|
603
|
+
|
604
|
+
## Testing Plan
|
605
|
+
|
606
|
+
1. Unit tests for AI command class
|
607
|
+
2. Integration tests for action execution
|
608
|
+
3. Mock API responses for testing
|
609
|
+
4. Test with various prompt patterns
|
610
|
+
5. Test configuration storage and retrieval
|
611
|
+
6. Test error handling and edge cases
|
data/delete_notebooks.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "lib/ruby_todo"
|
5
|
+
require_relative "lib/ruby_todo/database"
|
6
|
+
|
7
|
+
# Setup the database connection
|
8
|
+
RubyTodo::Database.setup
|
9
|
+
|
10
|
+
# Count notebooks and tasks before deletion
|
11
|
+
notebook_count = RubyTodo::Notebook.count
|
12
|
+
task_count = RubyTodo::Task.count
|
13
|
+
|
14
|
+
# Delete tasks first to avoid foreign key constraint errors
|
15
|
+
RubyTodo::Task.delete_all
|
16
|
+
puts "Successfully deleted #{task_count} tasks."
|
17
|
+
|
18
|
+
# Then delete notebooks
|
19
|
+
RubyTodo::Notebook.delete_all
|
20
|
+
puts "Successfully deleted #{notebook_count} notebooks."
|