anki_generator 0.1.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76f0b2892551729509fe0d6568b9e78994ee6b0e4b8001aafe904a76a6fe6418
4
- data.tar.gz: b4da700e556de1d07a7ebf8dc82859f2a14644ed18c5f35c0c8a22527e474bd4
3
+ metadata.gz: c8124d9ca11c2259790dfabfb10f1fab725cfa4889300d6bf91353cce45b9604
4
+ data.tar.gz: 6be84b8c3d8d430c91ad7c02f0fb063cc3e5ea27c35ee946d2c3ea091aedb994
5
5
  SHA512:
6
- metadata.gz: c8146c58bf49b3d77a93f739eb2f94669c23c2d66e62370c4dc755541bb087900e2516d331edd9e6b9ae26ff813f56890010f1edf89bfce05131166b95362687
7
- data.tar.gz: cbd79503bfd3b4d33b50db4398fbb4ff3d742e355e3f0113a061eacf76fc04d20d22e5402109b8c5c82dccb46864904f3b28a8a055671ff683335023f7007952
6
+ metadata.gz: 1bae0bd4aad23ab158031ab876385a10660559b5a9acca2be6a4ba932a2851e7c468e5b18d5db33f57724198fe8fda3e6478dfec05718f1a15703fd3a188bf1b
7
+ data.tar.gz: 25c74048b6dcd2428aab8f879cfebd41b81d7264bd3a5f8e8805530563b7099b9ee72be220e97fe1af070aa44460a084d20fa8010ea658e686a031415c1c2f90
data/README.md ADDED
@@ -0,0 +1,428 @@
1
+ # Anki Generator
2
+
3
+ A Ruby tool to generate Anki .apkg files from YAML definitions with AI-powered content generation using OpenRouter and file attachment support.
4
+
5
+ ## Features
6
+
7
+ - **AI-Powered Generation**: Create flashcards using OpenRouter API with multiple AI models (GPT, Claude, Llama, etc.)
8
+ - **File Attachment Support**: Attach code files, documentation, or entire directories for context-aware generation
9
+ - **Prompt File Support**: Use text files as prompts for better organization and reusability
10
+ - **Multiple Input Methods**: Generate from YAML files, direct prompts, or file attachments
11
+ - **Intelligent Content Processing**: Automatic text file detection, size limits, and binary file filtering
12
+ - **Direct Prompt-to-Deck Generation**: Create decks in one command without intermediate files
13
+ - **Sync Functionality**: Merge new AI-generated cards with existing decks
14
+ - **Flexible Configuration**: Multiple difficulty levels, context settings, and model selection
15
+ - **Comprehensive CLI**: Full command-line interface with extensive options
16
+
17
+ ## Installation
18
+
19
+ 1. Clone the repository
20
+ 2. Install dependencies:
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ 3. Set up your OpenRouter API key:
26
+ ```bash
27
+ cp .env.example .env
28
+ # Edit .env and add your OpenRouter API key
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### Quick Start - Generate from Prompt
34
+
35
+ The fastest way to create flashcards is directly from a prompt:
36
+
37
+ ```bash
38
+ # Generate flashcards and create deck in one step
39
+ ./bin/anki_generator prompt_to_deck "Ruby programming basics" "Ruby Deck" ruby_deck.apkg --api_key YOUR_API_KEY
40
+
41
+ # Generate from a prompt file with code attachments
42
+ ./bin/anki_generator prompt_to_deck study_prompt.txt "Code Study" code_deck.apkg --prompt-file --attach ./src --api_key YOUR_API_KEY
43
+
44
+ # Generate just the YAML file first
45
+ ./bin/anki_generator generate_yaml "JavaScript fundamentals" js_cards.yaml --api_key YOUR_API_KEY --count 15
46
+
47
+ # Then create the deck
48
+ ./bin/anki_generator generate "JS Deck" js_cards.yaml js_deck.apkg
49
+ ```
50
+
51
+ ### Traditional Usage
52
+
53
+ Generate a deck from a YAML file:
54
+
55
+ ```bash
56
+ ./bin/anki_generator generate "My Deck" input/input.yaml my_deck.apkg
57
+ ```
58
+
59
+ ### AI-Powered Generation
60
+
61
+ Create an AI generation template:
62
+
63
+ ```bash
64
+ ./bin/anki_generator create_ai_template my_ai_deck.yaml
65
+ ```
66
+
67
+ Generate a deck with AI content:
68
+
69
+ ```bash
70
+ ./bin/anki_generator generate "AI Deck" my_ai_deck.yaml ai_deck.apkg --api_key YOUR_API_KEY
71
+ ```
72
+
73
+ ### Advanced Options
74
+
75
+ ```bash
76
+ # Use a specific AI model
77
+ ./bin/anki_generator prompt_to_deck "Python basics" "Python Deck" python.apkg --model anthropic/claude-3-sonnet
78
+
79
+ # Set difficulty and count
80
+ ./bin/anki_generator generate_yaml "Advanced algorithms" algo.yaml --difficulty hard --count 20
81
+
82
+ # Add context for better generation
83
+ ./bin/anki_generator prompt_to_deck "Machine Learning" "ML Deck" ml.apkg --context "For computer science students" --difficulty medium
84
+
85
+ # Use prompt from file
86
+ ./bin/anki_generator generate_yaml prompt.txt output.yaml --prompt-file
87
+
88
+ # Attach files for context
89
+ ./bin/anki_generator prompt_to_deck "Explain this code" "Code Deck" code.apkg --attach src/main.rb --attach config.yml
90
+
91
+ # Attach entire directory
92
+ ./bin/anki_generator generate_yaml "Create cards about this project" project.yaml --attach ./src --attach ./docs
93
+
94
+ # Combine file prompt with attachments
95
+ ./bin/anki_generator prompt_to_deck prompt.txt "My Deck" deck.apkg --prompt-file --attach ./examples
96
+
97
+ # Save intermediate YAML file
98
+ ./bin/anki_generator prompt_to_deck "React hooks" "React Deck" react.apkg --save-yaml
99
+
100
+ # Sync with existing deck
101
+ ./bin/anki_generator generate "My Deck" input.yaml output.apkg --sync_with existing_deck.yaml
102
+
103
+ # Test API connection
104
+ ./bin/anki_generator test_api --api_key YOUR_API_KEY
105
+ ```
106
+
107
+ ## CLI Commands
108
+
109
+ ### `prompt_to_deck` - One-Step Generation
110
+ Generate flashcards from a prompt and create the deck immediately:
111
+ ```bash
112
+ anki_generator prompt_to_deck PROMPT DECK_NAME OUTPUT_FILE [options]
113
+ ```
114
+
115
+ **Options:**
116
+ - `--attach FILE_OR_DIR [FILE_OR_DIR...]` - Attach files or directories for context
117
+ - `--prompt-file` - Treat PROMPT as a file path to read from
118
+ - `--save-yaml` - Save intermediate YAML file
119
+ - `--api-key API_KEY` - OpenRouter API key
120
+ - `--model MODEL` - AI model to use
121
+ - `--difficulty LEVEL` - Difficulty level (easy, medium, hard)
122
+ - `--count N` - Number of flashcards to generate
123
+ - `--context TEXT` - Additional context for better generation
124
+
125
+ ### `generate_yaml` - Generate YAML from Prompt
126
+ Create a YAML file from a prompt for later use:
127
+ ```bash
128
+ anki_generator generate_yaml PROMPT OUTPUT_YAML [options]
129
+ ```
130
+
131
+ **Options:**
132
+ - `--attach FILE_OR_DIR [FILE_OR_DIR...]` - Attach files or directories for context
133
+ - `--prompt-file` - Treat PROMPT as a file path to read from
134
+ - `--api-key API_KEY` - OpenRouter API key
135
+ - `--model MODEL` - AI model to use
136
+ - `--difficulty LEVEL` - Difficulty level (easy, medium, hard)
137
+ - `--count N` - Number of flashcards to generate
138
+ - `--context TEXT` - Additional context for better generation
139
+
140
+ ### `generate` - Create Deck from YAML
141
+ Generate an Anki deck from an existing YAML file:
142
+ ```bash
143
+ anki_generator generate DECK_NAME YAML_FILE OUTPUT_FILE [options]
144
+ ```
145
+
146
+ ### `create_ai_template` - Create Template
147
+ Create a template YAML file for AI generation:
148
+ ```bash
149
+ anki_generator create_ai_template TEMPLATE_FILE
150
+ ```
151
+
152
+ ### `test_api` - Test Connection
153
+ Test your OpenRouter API connection:
154
+ ```bash
155
+ anki_generator test_api [options]
156
+ ```
157
+
158
+ ## File Attachments
159
+
160
+ The Anki Generator supports attaching files and directories to provide context for AI generation. This is particularly useful for:
161
+
162
+ - Creating flashcards about specific code files
163
+ - Generating questions based on documentation
164
+ - Learning from configuration files or data structures
165
+ - Creating study materials from existing project files
166
+
167
+ ### Supported File Types
168
+
169
+ The tool automatically processes text-based files including:
170
+ - Code files: `.rb`, `.py`, `.js`, `.ts`, `.java`, `.cpp`, `.c`, `.go`, `.rs`, etc.
171
+ - Documentation: `.md`, `.txt`, `.rst`, `.adoc`, `.org`
172
+ - Configuration: `.yaml`, `.yml`, `.json`, `.xml`, `.toml`
173
+ - Web files: `.html`, `.css`, `.scss`
174
+ - Scripts: `.sh`, `.bat`, `.ps1`
175
+
176
+ ### File Size Limits
177
+
178
+ - Maximum individual file size: 1MB
179
+ - Maximum total attachment size: 5MB
180
+ - Files exceeding limits are automatically skipped with warnings
181
+
182
+ ### Usage Examples
183
+
184
+ ```bash
185
+ # Attach a single file
186
+ ./bin/anki_generator prompt_to_deck "Explain this Ruby class" "Ruby Deck" ruby.apkg --attach person.rb
187
+
188
+ # Attach multiple files
189
+ ./bin/anki_generator generate_yaml "Create cards about these configs" config.yaml --attach database.yml --attach routes.rb --attach Gemfile
190
+
191
+ # Attach entire directories (processes all text files)
192
+ ./bin/anki_generator prompt_to_deck "Study this codebase" "Project Deck" project.apkg --attach ./src --attach ./lib
193
+
194
+ # Combine with prompt files
195
+ ./bin/anki_generator generate_yaml study_prompt.txt output.yaml --prompt-file --attach ./examples --attach README.md
196
+
197
+ # Use context and attachments together
198
+ ./bin/anki_generator prompt_to_deck "Advanced Ruby patterns" "Advanced Ruby" advanced.apkg \
199
+ --attach ./lib --context "Focus on metaprogramming and DSL patterns" --difficulty hard
200
+ ```
201
+
202
+ ### How Attachments Work
203
+
204
+ 1. **File Processing**: The tool scans attached files and directories for text-based content
205
+ 2. **Content Inclusion**: File contents are included in the AI prompt with clear file boundaries
206
+ 3. **Smart Generation**: The AI uses the file content to create more accurate and specific flashcards
207
+ 4. **Automatic Filtering**: Binary files and oversized files are automatically skipped
208
+
209
+ ### Best Practices
210
+
211
+ - **Be Specific**: Attach only relevant files to avoid overwhelming the AI with too much context
212
+ - **Use Descriptive Prompts**: Combine attachments with clear prompts about what you want to learn
213
+ - **Organize Files**: Group related files in directories for easier attachment
214
+ - **Check File Sizes**: Large files will be skipped, so break them down if needed
215
+
216
+ ## YAML Formats
217
+
218
+ ### Traditional Format
219
+
220
+ ```yaml
221
+ - front: "What is Big O notation?"
222
+ back: "Big O notation describes the limiting behavior of a function..."
223
+
224
+ - front: "Define a graph"
225
+ back: "A graph is a collection of vertices connected by edges"
226
+ ```
227
+
228
+ ### AI Generation Format
229
+
230
+ ```yaml
231
+ ai_generation:
232
+ topics:
233
+ - "Ruby programming fundamentals"
234
+ - "Object-oriented programming"
235
+ context: "Beginner-friendly programming concepts"
236
+ difficulty: "medium" # easy, medium, hard
237
+ count: 10
238
+ save_generated: true
239
+
240
+ # Optional manual cards
241
+ cards:
242
+ - front: "Manual question"
243
+ back: "Manual answer"
244
+ ```
245
+
246
+ ## Configuration
247
+
248
+ ### Environment Variables
249
+
250
+ - `OPENROUTER_API_KEY`: Your OpenRouter API key
251
+ - `OPENROUTER_DEFAULT_MODEL`: Default model to use (optional)
252
+
253
+ ### Supported Models
254
+
255
+ The tool supports all models available through OpenRouter:
256
+
257
+ - OpenAI: `openai/gpt-4`, `openai/gpt-3.5-turbo`
258
+ - Anthropic: `anthropic/claude-3-sonnet`, `anthropic/claude-3-haiku`
259
+ - Meta: `meta-llama/llama-3.1-8b-instruct`
260
+ - And many more...
261
+
262
+ ## Examples
263
+
264
+ ### Quick Examples
265
+
266
+ ```bash
267
+ # Generate 10 Python flashcards
268
+ ./bin/anki_generator prompt_to_deck "Python data structures" "Python Deck" python.apkg
269
+
270
+ # Generate 20 hard-level algorithm cards
271
+ ./bin/anki_generator generate_yaml "Sorting algorithms" algo.yaml --difficulty hard --count 20
272
+
273
+ # Create deck with context
274
+ ./bin/anki_generator prompt_to_deck "React components" "React Deck" react.apkg --context "For beginners learning React"
275
+
276
+ # Study a specific code file
277
+ ./bin/anki_generator prompt_to_deck "Create flashcards about this Ruby class" "Ruby Class Deck" ruby_class.apkg --attach person.rb
278
+
279
+ # Learn from project documentation
280
+ ./bin/anki_generator generate_yaml "Study this API documentation" api_study.yaml --attach README.md --attach docs/api.md
281
+
282
+ # Comprehensive project study
283
+ ./bin/anki_generator prompt_to_deck "Help me understand this codebase structure and patterns" "Codebase Study" study.apkg \
284
+ --attach ./src --attach ./lib --attach README.md --context "Focus on architecture and design patterns"
285
+ ```
286
+
287
+ ### File Attachment Examples
288
+
289
+ ```bash
290
+ # Study a single Ruby file
291
+ ./bin/anki_generator prompt_to_deck "What does this code do?" "Code Analysis" analysis.apkg --attach calculator.rb
292
+
293
+ # Learn from configuration files
294
+ ./bin/anki_generator generate_yaml "Explain these configurations" config_study.yaml --attach config/database.yml --attach config/routes.rb
295
+
296
+ # Study an entire project
297
+ ./bin/anki_generator prompt_to_deck "Create comprehensive study cards for this project" "Project Study" project.apkg \
298
+ --attach ./app --attach ./lib --attach Gemfile --attach README.md
299
+
300
+ # Use prompt file with attachments
301
+ echo "Create flashcards focusing on the class structure and methods in the attached files" > study_prompt.txt
302
+ ./bin/anki_generator generate_yaml study_prompt.txt class_study.yaml --prompt-file --attach ./models
303
+
304
+ # Advanced usage with multiple options
305
+ ./bin/anki_generator prompt_to_deck prompt_file.txt "Advanced Study" advanced.apkg \
306
+ --prompt-file --attach ./src --attach ./docs --difficulty hard --count 25 \
307
+ --context "Focus on advanced patterns and best practices" --save-yaml
308
+ ```
309
+
310
+ ### Example YAML Files
311
+
312
+ See the `input/` directory for example YAML files, or create examples with `rake examples`:
313
+
314
+ - `examples/study_prompt.txt` - Example prompt file
315
+ - `examples/example_class.rb` - Example Ruby code for attachments
316
+ - `examples/manual_cards.yaml` - Traditional format example
317
+ - `input/ai_generation_example.yaml` - AI generation example
318
+ - `input/algorithms_ai.yaml` - Computer science topics
319
+ - `input/input.yaml.example` - Traditional format
320
+
321
+ ## Development
322
+
323
+ ## Development
324
+
325
+ ### Quick Start for Developers
326
+
327
+ ```bash
328
+ # Clone and setup
329
+ git clone <repository-url>
330
+ cd anki_generator
331
+ rake setup # Install dependencies and create examples
332
+
333
+ # Run tests
334
+ rake test # Run all tests
335
+ rake test_file[cli] # Run specific test
336
+
337
+ # Try the examples
338
+ rake demo_api # Test API connection
339
+ rake examples # Create example files
340
+ rake demo_attachments # Demo with file attachments
341
+ ```
342
+
343
+ ### Running Tests
344
+
345
+ ```bash
346
+ # Run all tests
347
+ rake test
348
+
349
+ # Run specific test file
350
+ rake test_file[cli] # runs tests/test_cli.rb
351
+ rake test_file[file_processor] # runs tests/test_file_processor.rb
352
+
353
+ # Run tests with coverage
354
+ rake test_coverage
355
+ ```
356
+
357
+ ### Development Commands
358
+
359
+ ```bash
360
+ # Setup development environment
361
+ rake setup # Install deps and create examples
362
+
363
+ # Build and install
364
+ rake build # Build gem
365
+ rake install_local # Build and install gem locally
366
+ rake clean # Clean build artifacts
367
+
368
+ # Code quality
369
+ rake lint # Run RuboCop linter
370
+ rake lint_fix # Auto-fix linting issues
371
+
372
+ # Demos and examples
373
+ rake examples # Create example files
374
+ rake demo_prompt # Demo prompt generation
375
+ rake demo_attachments # Demo file attachment features
376
+ rake demo_api # Test API connection
377
+
378
+ # Utilities
379
+ rake help # Show CLI help
380
+ rake version # Show version info
381
+ rake release_prep # Prepare for release
382
+ ```
383
+
384
+ ### Project Structure
385
+
386
+ ```
387
+ ├── lib/
388
+ │ ├── anki_generator.rb # Main generator class
389
+ │ ├── openrouter_client.rb # OpenRouter API client
390
+ │ ├── anki_cli.rb # CLI interface
391
+ │ └── file_processor.rb # File attachment processing
392
+ ├── bin/
393
+ │ └── anki_generator # CLI executable
394
+ ├── tests/ # Test files
395
+ ├── input/ # Example YAML files
396
+ └── README.md
397
+ ```
398
+
399
+ ## API Integration
400
+
401
+ The tool integrates with OpenRouter to provide access to multiple AI models. You can:
402
+
403
+ 1. Generate flashcards on any topic
404
+ 2. Specify difficulty levels
405
+ 3. Provide context for better generation
406
+ 4. Choose from various AI models
407
+ 5. Control the number of cards generated
408
+
409
+ ## Sync Functionality
410
+
411
+ The sync feature allows you to:
412
+
413
+ - Merge new AI-generated cards with existing manual cards
414
+ - Prevent duplicate cards (based on front text)
415
+ - Maintain version control of your flashcard sets
416
+ - Incrementally build large decks
417
+
418
+ ## Contributing
419
+
420
+ 1. Fork the repository
421
+ 2. Create a feature branch
422
+ 3. Add tests for new functionality
423
+ 4. Ensure all tests pass
424
+ 5. Submit a pull request
425
+
426
+ ## License
427
+
428
+ MIT License - see LICENSE file for details.
data/bin/anki_generator CHANGED
@@ -1,15 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'thor'
4
- require_relative '../lib/anki_generator'
5
-
6
- class AnkiCLI < Thor
7
- desc 'generate DECK_NAME YAML_FILE OUTPUT_FILE', 'Generate an Anki .apkg deck from a YAML file'
8
- def generate(deck_name, yaml_file, output_file)
9
- anki_generator = AnkiGenerator.new(name: deck_name, deck_file: yaml_file)
10
- anki_generator.generate_apkg(output_path: output_file)
11
- puts "Anki deck '#{deck_name}' has been successfully created as #{output_file}!"
12
- end
13
- end
3
+ require_relative '../lib/anki_cli'
14
4
 
15
5
  AnkiCLI.start(ARGV)
data/lib/anki_cli.rb ADDED
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'dotenv/load'
5
+ require_relative 'anki_generator'
6
+ require_relative 'file_processor'
7
+
8
+ class AnkiCLI < Thor
9
+ desc 'generate DECK_NAME YAML_FILE OUTPUT_FILE', 'Generate an Anki .apkg deck from a YAML file'
10
+ option :api_key, type: :string, desc: 'OpenRouter API key (or set OPENROUTER_API_KEY env var)'
11
+ option :model, type: :string, default: 'openai/gpt-3.5-turbo', desc: 'AI model to use'
12
+ option :sync_with, type: :string, desc: 'Existing YAML file to sync with'
13
+ def generate(deck_name, yaml_file, output_file)
14
+ anki_generator = AnkiGenerator.new(
15
+ name: deck_name,
16
+ deck_file: yaml_file,
17
+ api_key: options[:api_key],
18
+ model: options[:model]
19
+ )
20
+
21
+ anki_generator.sync_with_existing_deck(options[:sync_with]) if options[:sync_with]
22
+ anki_generator.generate_apkg(output_path: output_file)
23
+
24
+ puts "Anki deck '#{deck_name}' has been successfully created as #{output_file}!"
25
+ puts "Total cards: #{anki_generator.cards.length}"
26
+ end
27
+
28
+ desc 'generate_yaml PROMPT OUTPUT_YAML', 'Generate a YAML file from a prompt using AI'
29
+ option :api_key, type: :string, desc: 'OpenRouter API key (or set OPENROUTER_API_KEY env var)'
30
+ option :model, type: :string, default: 'openai/gpt-3.5-turbo', desc: 'AI model to use'
31
+ option :difficulty, type: :string, default: 'medium', desc: 'Difficulty level (easy, medium, hard)'
32
+ option :count, type: :numeric, default: 10, desc: 'Number of flashcards to generate'
33
+ option :context, type: :string, desc: 'Additional context for better generation'
34
+ option :attach, type: :array, desc: 'Attach files or directories for context'
35
+ option :prompt_file, type: :boolean, default: false, desc: 'Treat PROMPT as a file path to read from'
36
+ def generate_yaml(prompt, output_yaml)
37
+ begin
38
+ require_relative 'openrouter_client'
39
+ client = OpenRouterClient.new(api_key: options[:api_key], model: options[:model])
40
+
41
+ # Handle prompt from file
42
+ actual_prompt = if options[:prompt_file]
43
+ puts "📄 Reading prompt from file: #{prompt}"
44
+ FileProcessor.read_prompt_from_file(prompt)
45
+ else
46
+ prompt
47
+ end
48
+
49
+ # Process attachments
50
+ attachments = nil
51
+ if options[:attach]
52
+ puts "📎 Processing attachments..."
53
+ attachments = FileProcessor.process_attachments(options[:attach])
54
+ end
55
+
56
+ puts "Generating flashcards for: #{actual_prompt[0..100]}#{'...' if actual_prompt.length > 100}"
57
+ puts "Model: #{client.model}"
58
+ puts "Difficulty: #{options[:difficulty]}"
59
+ puts "Count: #{options[:count]}"
60
+ puts "Attachments: #{attachments&.length || 0} file(s)" if attachments
61
+ puts "Generating..."
62
+
63
+ # Generate flashcards using the prompt as topics
64
+ cards = client.generate_multiple_flashcards(
65
+ topics: [actual_prompt],
66
+ context: options[:context],
67
+ difficulty: options[:difficulty],
68
+ count: options[:count],
69
+ attachments: attachments
70
+ )
71
+
72
+ # Create YAML structure
73
+ yaml_content = {
74
+ 'ai_generation' => {
75
+ 'topics' => [actual_prompt],
76
+ 'context' => options[:context],
77
+ 'difficulty' => options[:difficulty],
78
+ 'count' => options[:count],
79
+ 'save_generated' => false
80
+ },
81
+ 'cards' => cards
82
+ }
83
+
84
+ # Write to file
85
+ File.open(output_yaml, 'w') do |file|
86
+ file.write(yaml_content.to_yaml)
87
+ end
88
+
89
+ puts "✅ Generated #{cards.length} flashcards!"
90
+ puts "YAML file created: #{output_yaml}"
91
+ puts ""
92
+ puts "Preview of generated cards:"
93
+ cards.first(3).each_with_index do |card, index|
94
+ puts "#{index + 1}. #{card['front']}"
95
+ puts " → #{card['back'][0..100]}#{'...' if card['back'].length > 100}"
96
+ puts ""
97
+ end
98
+
99
+ puts "To create an Anki deck, run:"
100
+ puts " anki_generator generate \"My Deck\" #{output_yaml} my_deck.apkg"
101
+
102
+ rescue => e
103
+ puts "❌ Error generating flashcards: #{e.message}"
104
+ exit 1
105
+ end
106
+ end
107
+
108
+ desc 'prompt_to_deck PROMPT DECK_NAME OUTPUT_FILE', 'Generate flashcards from prompt and create deck in one step'
109
+ option :api_key, type: :string, desc: 'OpenRouter API key (or set OPENROUTER_API_KEY env var)'
110
+ option :model, type: :string, default: 'openai/gpt-3.5-turbo', desc: 'AI model to use'
111
+ option :difficulty, type: :string, default: 'medium', desc: 'Difficulty level (easy, medium, hard)'
112
+ option :count, type: :numeric, default: 10, desc: 'Number of flashcards to generate'
113
+ option :context, type: :string, desc: 'Additional context for better generation'
114
+ option :save_yaml, type: :boolean, default: false, desc: 'Save intermediate YAML file'
115
+ option :attach, type: :array, desc: 'Attach files or directories for context'
116
+ option :prompt_file, type: :boolean, default: false, desc: 'Treat PROMPT as a file path to read from'
117
+ def prompt_to_deck(prompt, deck_name, output_file)
118
+ begin
119
+ require_relative 'openrouter_client'
120
+ client = OpenRouterClient.new(api_key: options[:api_key], model: options[:model])
121
+
122
+ # Handle prompt from file
123
+ actual_prompt = if options[:prompt_file]
124
+ puts "📄 Reading prompt from file: #{prompt}"
125
+ FileProcessor.read_prompt_from_file(prompt)
126
+ else
127
+ prompt
128
+ end
129
+
130
+ # Process attachments
131
+ attachments = nil
132
+ if options[:attach]
133
+ puts "📎 Processing attachments..."
134
+ attachments = FileProcessor.process_attachments(options[:attach])
135
+ end
136
+
137
+ puts "🚀 Generating Anki deck from prompt: #{actual_prompt[0..100]}#{'...' if actual_prompt.length > 100}"
138
+ puts "Model: #{client.model}"
139
+ puts "Difficulty: #{options[:difficulty]}"
140
+ puts "Count: #{options[:count]}"
141
+ puts "Attachments: #{attachments&.length || 0} file(s)" if attachments
142
+ puts ""
143
+
144
+ # Generate flashcards
145
+ puts "Generating flashcards..."
146
+ cards = client.generate_multiple_flashcards(
147
+ topics: [actual_prompt],
148
+ context: options[:context],
149
+ difficulty: options[:difficulty],
150
+ count: options[:count],
151
+ attachments: attachments
152
+ )
153
+
154
+ puts "✅ Generated #{cards.length} flashcards!"
155
+
156
+ # Create temporary YAML file
157
+ temp_yaml = "temp_#{Time.now.to_i}.yaml"
158
+ yaml_content = {
159
+ 'ai_generation' => {
160
+ 'topics' => [actual_prompt],
161
+ 'context' => options[:context],
162
+ 'difficulty' => options[:difficulty],
163
+ 'count' => options[:count],
164
+ 'save_generated' => false
165
+ },
166
+ 'cards' => cards
167
+ }
168
+
169
+ File.open(temp_yaml, 'w') do |file|
170
+ file.write(yaml_content.to_yaml)
171
+ end
172
+
173
+ # Generate Anki deck
174
+ puts "Creating Anki deck..."
175
+ anki_generator = AnkiGenerator.new(
176
+ name: deck_name,
177
+ deck_file: temp_yaml,
178
+ api_key: options[:api_key],
179
+ model: options[:model]
180
+ )
181
+
182
+ anki_generator.generate_apkg(output_path: output_file)
183
+
184
+ # Clean up or save YAML
185
+ if options[:save_yaml]
186
+ yaml_file = output_file.gsub('.apkg', '.yaml')
187
+ File.rename(temp_yaml, yaml_file)
188
+ puts "YAML file saved: #{yaml_file}"
189
+ else
190
+ File.delete(temp_yaml)
191
+ end
192
+
193
+ puts "🎉 Anki deck '#{deck_name}' created successfully!"
194
+ puts "File: #{output_file}"
195
+ puts "Total cards: #{cards.length}"
196
+ puts ""
197
+ puts "Preview of generated cards:"
198
+ cards.first(3).each_with_index do |card, index|
199
+ puts "#{index + 1}. #{card['front']}"
200
+ puts " → #{card['back'][0..100]}#{'...' if card['back'].length > 100}"
201
+ puts ""
202
+ end
203
+
204
+ rescue => e
205
+ puts "❌ Error: #{e.message}"
206
+ exit 1
207
+ end
208
+ end
209
+
210
+ desc 'create_ai_template TEMPLATE_FILE', 'Create a template YAML file for AI generation'
211
+ def create_ai_template(template_file)
212
+ template_content = {
213
+ 'ai_generation' => {
214
+ 'topics' => ['Example Topic 1', 'Example Topic 2'],
215
+ 'context' => 'Additional context for generating flashcards',
216
+ 'difficulty' => 'medium',
217
+ 'count' => 5,
218
+ 'save_generated' => true
219
+ },
220
+ 'cards' => [
221
+ {
222
+ 'front' => 'Example manual card front',
223
+ 'back' => 'Example manual card back'
224
+ }
225
+ ]
226
+ }
227
+
228
+ File.open(template_file, 'w') do |file|
229
+ file.write(template_content.to_yaml)
230
+ end
231
+
232
+ puts "AI generation template created: #{template_file}"
233
+ puts "Edit the file and run 'anki_generator generate' to create your deck!"
234
+ end
235
+
236
+ desc 'test_api', 'Test OpenRouter API connection'
237
+ option :api_key, type: :string, desc: 'OpenRouter API key (or set OPENROUTER_API_KEY env var)'
238
+ option :model, type: :string, default: 'openai/gpt-3.5-turbo', desc: 'AI model to use'
239
+ def test_api
240
+ begin
241
+ require_relative 'openrouter_client'
242
+ client = OpenRouterClient.new(api_key: options[:api_key], model: options[:model])
243
+
244
+ puts "Testing OpenRouter API connection..."
245
+ puts "Model: #{client.model}"
246
+
247
+ card = client.generate_flashcard(topic: 'Ruby programming', difficulty: 'easy')
248
+
249
+ puts "✅ API connection successful!"
250
+ puts "Sample generated card:"
251
+ puts "Front: #{card['front']}"
252
+ puts "Back: #{card['back']}"
253
+
254
+ rescue => e
255
+ puts "❌ API test failed: #{e.message}"
256
+ exit 1
257
+ end
258
+ end
259
+ end
@@ -2,21 +2,87 @@
2
2
 
3
3
  require 'yaml'
4
4
  require 'anki2'
5
+ require_relative 'openrouter_client'
5
6
 
6
7
  # AnkiGenerator -> Class
7
8
  class AnkiGenerator
8
- attr_accessor :deck_file, :cards, :name
9
+ attr_accessor :deck_file, :cards, :name, :openrouter_client
9
10
 
10
- def initialize(deck_file:, name:)
11
+ def initialize(deck_file:, name:, api_key: nil, model: 'openai/gpt-3.5-turbo')
11
12
  self.deck_file = deck_file
12
13
  self.name = name
13
14
  self.cards = []
15
+ self.openrouter_client = OpenRouterClient.new(api_key: api_key, model: model) if api_key || ENV['OPENROUTER_API_KEY']
14
16
 
15
17
  fill_cards
16
18
  end
17
19
 
18
20
  def fill_cards
19
- self.cards = YAML.load_file(deck_file)
21
+ yaml_content = YAML.load_file(deck_file)
22
+
23
+ # Handle both old format (array of cards) and new format (with ai_generation config)
24
+ if yaml_content.is_a?(Hash) && yaml_content['ai_generation']
25
+ process_ai_generation(yaml_content)
26
+ else
27
+ self.cards = yaml_content.is_a?(Array) ? yaml_content : []
28
+ end
29
+ end
30
+
31
+ def process_ai_generation(config, attachments: nil)
32
+ ai_config = config['ai_generation']
33
+ existing_cards = config['cards'] || []
34
+
35
+ # Generate AI cards if specified and client is available
36
+ if ai_config['topics'] && openrouter_client
37
+ ai_cards = generate_ai_cards(
38
+ topics: ai_config['topics'],
39
+ context: ai_config['context'],
40
+ difficulty: ai_config['difficulty'] || 'medium',
41
+ count: ai_config['count'] || 5,
42
+ attachments: attachments
43
+ )
44
+
45
+ self.cards = existing_cards + ai_cards
46
+ else
47
+ self.cards = existing_cards
48
+ end
49
+
50
+ # Save the generated cards back to YAML for future reference
51
+ save_generated_cards_to_yaml(config) if ai_config['save_generated'] && openrouter_client
52
+ end
53
+
54
+ def generate_ai_cards(topics:, context: nil, difficulty: 'medium', count: 5, attachments: nil)
55
+ if topics.is_a?(Array) && topics.length > 1
56
+ openrouter_client.generate_multiple_flashcards(
57
+ topics: topics,
58
+ context: context,
59
+ difficulty: difficulty,
60
+ count: count,
61
+ attachments: attachments
62
+ )
63
+ else
64
+ topic = topics.is_a?(Array) ? topics.first : topics
65
+ [openrouter_client.generate_flashcard(
66
+ topic: topic,
67
+ context: context,
68
+ difficulty: difficulty,
69
+ attachments: attachments
70
+ )]
71
+ end
72
+ end
73
+
74
+ def save_generated_cards_to_yaml(original_config)
75
+ output_file = deck_file.gsub('.yaml', '_generated.yaml')
76
+
77
+ updated_config = original_config.dup
78
+ updated_config['cards'] = cards
79
+ updated_config['ai_generation']['save_generated'] = false # Prevent recursive generation
80
+
81
+ File.open(output_file, 'w') do |file|
82
+ file.write(updated_config.to_yaml)
83
+ end
84
+
85
+ puts "Generated cards saved to: #{output_file}"
20
86
  end
21
87
 
22
88
  def generate_apkg(output_path:)
@@ -28,4 +94,23 @@ class AnkiGenerator
28
94
 
29
95
  deck.save
30
96
  end
97
+
98
+ def add_card(front:, back:)
99
+ cards << { 'front' => front, 'back' => back }
100
+ end
101
+
102
+ def sync_with_existing_deck(existing_yaml_file)
103
+ return unless File.exist?(existing_yaml_file)
104
+
105
+ existing_cards = YAML.load_file(existing_yaml_file)
106
+ existing_cards = existing_cards.is_a?(Array) ? existing_cards : existing_cards['cards'] || []
107
+
108
+ # Simple deduplication based on front text
109
+ existing_fronts = existing_cards.map { |card| card['front'] }
110
+ new_cards = cards.reject { |card| existing_fronts.include?(card['front']) }
111
+
112
+ self.cards = existing_cards + new_cards
113
+
114
+ puts "Synced #{new_cards.length} new cards with existing deck"
115
+ end
31
116
  end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ # FileProcessor -> Handles file and directory processing for attachments
6
+ class FileProcessor
7
+ # Supported text file extensions
8
+ TEXT_EXTENSIONS = %w[.txt .md .rb .py .js .ts .java .cpp .c .h .hpp .css .html .xml .json .yaml .yml .sql .sh .bat .ps1 .php .go .rs .swift .kt .scala .clj .hs .elm .ex .exs .erl .pl .r .m .tex .org .rst .adoc].freeze
9
+
10
+ # Maximum file size in bytes (1MB)
11
+ MAX_FILE_SIZE = 1_048_576
12
+
13
+ # Maximum total content size (5MB)
14
+ MAX_TOTAL_SIZE = 5_242_880
15
+
16
+ def self.process_attachments(paths)
17
+ return [] if paths.nil? || paths.empty?
18
+
19
+ attachments = []
20
+ total_size = 0
21
+
22
+ paths.each do |path_str|
23
+ path = Pathname.new(path_str)
24
+
25
+ unless path.exist?
26
+ puts "⚠️ Warning: Path does not exist: #{path_str}"
27
+ next
28
+ end
29
+
30
+ if path.directory?
31
+ dir_attachments = process_directory(path)
32
+ dir_attachments.each do |attachment|
33
+ if total_size + attachment[:content].bytesize > MAX_TOTAL_SIZE
34
+ puts "⚠️ Warning: Total attachment size limit reached. Skipping remaining files."
35
+ break
36
+ end
37
+ attachments << attachment
38
+ total_size += attachment[:content].bytesize
39
+ end
40
+ elsif path.file?
41
+ attachment = process_file(path)
42
+ if attachment
43
+ if total_size + attachment[:content].bytesize > MAX_TOTAL_SIZE
44
+ puts "⚠️ Warning: Total attachment size limit reached. Skipping #{path_str}"
45
+ else
46
+ attachments << attachment
47
+ total_size += attachment[:content].bytesize
48
+ end
49
+ end
50
+ else
51
+ puts "⚠️ Warning: Path is neither file nor directory: #{path_str}"
52
+ end
53
+ end
54
+
55
+ puts "📎 Processed #{attachments.length} file(s) (#{format_size(total_size)})" if attachments.any?
56
+ attachments
57
+ end
58
+
59
+ def self.process_directory(dir_path)
60
+ attachments = []
61
+
62
+ # Find all text files in directory (non-recursive for now)
63
+ dir_path.children.each do |child|
64
+ next unless child.file?
65
+
66
+ attachment = process_file(child)
67
+ attachments << attachment if attachment
68
+ end
69
+
70
+ attachments
71
+ end
72
+
73
+ def self.process_file(file_path)
74
+ # Check file size
75
+ if file_path.size > MAX_FILE_SIZE
76
+ puts "⚠️ Warning: File too large, skipping: #{file_path} (#{format_size(file_path.size)})"
77
+ return nil
78
+ end
79
+
80
+ # Check if it's a text file
81
+ unless text_file?(file_path)
82
+ puts "⚠️ Warning: Non-text file, skipping: #{file_path}"
83
+ return nil
84
+ end
85
+
86
+ begin
87
+ content = file_path.read(encoding: 'UTF-8')
88
+
89
+ # Truncate if too long
90
+ if content.bytesize > MAX_FILE_SIZE
91
+ content = content.byteslice(0, MAX_FILE_SIZE) + "\n... [truncated]"
92
+ end
93
+
94
+ {
95
+ filename: file_path.basename.to_s,
96
+ path: file_path.to_s,
97
+ content: content
98
+ }
99
+ rescue => e
100
+ puts "⚠️ Warning: Could not read file #{file_path}: #{e.message}"
101
+ nil
102
+ end
103
+ end
104
+
105
+ def self.text_file?(file_path)
106
+ # Check extension
107
+ ext = file_path.extname.downcase
108
+ return true if TEXT_EXTENSIONS.include?(ext)
109
+
110
+ # For files without extension, try to detect if it's text
111
+ return false if file_path.extname.empty? && file_path.basename.to_s !~ /^[A-Z_]+$/
112
+
113
+ # Try to read first few bytes to detect binary files
114
+ begin
115
+ sample = file_path.read(512, encoding: 'BINARY')
116
+ # If it contains null bytes, it's likely binary
117
+ !sample.include?("\x00")
118
+ rescue
119
+ false
120
+ end
121
+ end
122
+
123
+ def self.format_size(bytes)
124
+ if bytes < 1024
125
+ "#{bytes} B"
126
+ elsif bytes < 1024 * 1024
127
+ "#{(bytes / 1024.0).round(1)} KB"
128
+ else
129
+ "#{(bytes / (1024.0 * 1024)).round(1)} MB"
130
+ end
131
+ end
132
+
133
+ def self.read_prompt_from_file(file_path)
134
+ path = Pathname.new(file_path)
135
+
136
+ unless path.exist?
137
+ raise "Prompt file does not exist: #{file_path}"
138
+ end
139
+
140
+ unless path.file?
141
+ raise "Prompt path is not a file: #{file_path}"
142
+ end
143
+
144
+ begin
145
+ content = path.read(encoding: 'UTF-8').strip
146
+
147
+ if content.empty?
148
+ raise "Prompt file is empty: #{file_path}"
149
+ end
150
+
151
+ content
152
+ rescue => e
153
+ raise "Could not read prompt file #{file_path}: #{e.message}"
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'json'
5
+ require 'dotenv/load'
6
+
7
+ # OpenRouterClient -> API client for OpenRouter
8
+ class OpenRouterClient
9
+ BASE_URL = 'https://openrouter.ai/api/v1'
10
+
11
+ attr_reader :api_key, :model
12
+
13
+ def initialize(api_key: nil, model: 'openai/gpt-3.5-turbo')
14
+ @api_key = api_key || ENV['OPENROUTER_API_KEY']
15
+ @model = model
16
+
17
+ raise ArgumentError, 'API key is required' if @api_key.nil? || @api_key.empty?
18
+ end
19
+
20
+ def generate_flashcard(topic:, context: nil, difficulty: 'medium', attachments: nil)
21
+ prompt = build_flashcard_prompt(topic: topic, context: context, difficulty: difficulty, attachments: attachments)
22
+
23
+ response = make_request(prompt)
24
+ parse_flashcard_response(response)
25
+ end
26
+
27
+ def generate_multiple_flashcards(topics:, context: nil, difficulty: 'medium', count: 5, attachments: nil)
28
+ prompt = build_multiple_flashcards_prompt(
29
+ topics: topics,
30
+ context: context,
31
+ difficulty: difficulty,
32
+ count: count,
33
+ attachments: attachments
34
+ )
35
+
36
+ response = make_request(prompt)
37
+ parse_multiple_flashcards_response(response)
38
+ end
39
+
40
+ private
41
+
42
+ def connection
43
+ @connection ||= Faraday.new(url: BASE_URL) do |conn|
44
+ conn.request :json
45
+ conn.response :json
46
+ conn.adapter Faraday.default_adapter
47
+ conn.headers['Authorization'] = "Bearer #{@api_key}"
48
+ conn.headers['Content-Type'] = 'application/json'
49
+ end
50
+ end
51
+
52
+ def make_request(prompt)
53
+ response = connection.post('/chat/completions') do |req|
54
+ req.body = {
55
+ model: @model,
56
+ messages: [
57
+ {
58
+ role: 'user',
59
+ content: prompt
60
+ }
61
+ ],
62
+ temperature: 0.7,
63
+ max_tokens: 1000
64
+ }
65
+ end
66
+
67
+ handle_response(response)
68
+ end
69
+
70
+ def handle_response(response)
71
+ unless response.success?
72
+ raise "OpenRouter API error: #{response.status} - #{response.body}"
73
+ end
74
+
75
+ response.body.dig('choices', 0, 'message', 'content')
76
+ end
77
+
78
+ def build_flashcard_prompt(topic:, context:, difficulty:, attachments:)
79
+ base_prompt = <<~PROMPT
80
+ Create a single flashcard for the topic: "#{topic}"
81
+ Difficulty level: #{difficulty}
82
+ #{context ? "Additional context: #{context}" : ''}
83
+
84
+ #{build_attachments_section(attachments) if attachments}
85
+
86
+ Format your response as JSON with exactly this structure:
87
+ {
88
+ "front": "Question or prompt",
89
+ "back": "Answer or explanation"
90
+ }
91
+
92
+ Make the flashcard educational and appropriate for the #{difficulty} difficulty level.
93
+ For the back side, provide a clear, concise explanation.
94
+ #{attachments ? "Use the provided file content to create more accurate and detailed flashcards." : ""}
95
+ PROMPT
96
+
97
+ base_prompt.strip
98
+ end
99
+
100
+ def build_multiple_flashcards_prompt(topics:, context:, difficulty:, count:, attachments:)
101
+ topics_list = topics.is_a?(Array) ? topics.join(', ') : topics.to_s
102
+
103
+ base_prompt = <<~PROMPT
104
+ Create #{count} flashcards covering these topics: #{topics_list}
105
+ Difficulty level: #{difficulty}
106
+ #{context ? "Additional context: #{context}" : ''}
107
+
108
+ #{build_attachments_section(attachments) if attachments}
109
+
110
+ Format your response as JSON with exactly this structure:
111
+ [
112
+ {
113
+ "front": "Question or prompt 1",
114
+ "back": "Answer or explanation 1"
115
+ },
116
+ {
117
+ "front": "Question or prompt 2",
118
+ "back": "Answer or explanation 2"
119
+ }
120
+ ]
121
+
122
+ Make the flashcards educational, diverse, and appropriate for the #{difficulty} difficulty level.
123
+ Ensure each flashcard covers different aspects of the topics.
124
+ #{attachments ? "Use the provided file content to create more accurate and detailed flashcards based on the specific information in the files." : ""}
125
+ PROMPT
126
+
127
+ base_prompt.strip
128
+ end
129
+
130
+ def parse_flashcard_response(response)
131
+ JSON.parse(response)
132
+ rescue JSON::ParserError => e
133
+ raise "Failed to parse OpenRouter response as JSON: #{e.message}"
134
+ end
135
+
136
+ def parse_multiple_flashcards_response(response)
137
+ parsed = JSON.parse(response)
138
+
139
+ # Ensure we return an array
140
+ parsed.is_a?(Array) ? parsed : [parsed]
141
+ rescue JSON::ParserError => e
142
+ raise "Failed to parse OpenRouter response as JSON: #{e.message}"
143
+ end
144
+
145
+ def build_attachments_section(attachments)
146
+ return "" unless attachments && !attachments.empty?
147
+
148
+ section = "=== ATTACHED FILE CONTENT ===\n\n"
149
+
150
+ attachments.each do |attachment|
151
+ section += "--- #{attachment[:filename]} ---\n"
152
+ section += "#{attachment[:content]}\n\n"
153
+ end
154
+
155
+ section += "=== END ATTACHED CONTENT ===\n"
156
+ section
157
+ end
158
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anki_generator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ceb
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-03 00:00:00.000000000 Z
11
+ date: 2026-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anki2
@@ -25,35 +25,108 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.1.2
27
27
  - !ruby/object:Gem::Dependency
28
- name: minitest
28
+ name: thor
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 5.25.1
33
+ version: '1.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 5.25.1
40
+ version: '1.2'
41
41
  - !ruby/object:Gem::Dependency
42
- name: thor
42
+ name: faraday
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.2'
47
+ version: '2.0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.2'
55
- description: AnkiGenerator is a command-line tool to create Anki decks in .apkg format
56
- from YAML files.
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: dotenv
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.8'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '5.25'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '5.25'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '13.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '13.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.0'
125
+ description: A powerful command-line tool that generates Anki flashcard decks (.apkg)
126
+ from YAML files, direct prompts, or file attachments. Features AI-powered content
127
+ generation using OpenRouter API with support for multiple models (GPT, Claude, Llama),
128
+ file and directory attachment processing for context-aware generation, prompt file
129
+ support, intelligent content filtering, and flexible deck management with sync capabilities.
57
130
  email:
58
131
  - ceeb.developer@gmail.com
59
132
  executables:
@@ -61,12 +134,21 @@ executables:
61
134
  extensions: []
62
135
  extra_rdoc_files: []
63
136
  files:
137
+ - README.md
64
138
  - bin/anki_generator
139
+ - lib/anki_cli.rb
65
140
  - lib/anki_generator.rb
66
- homepage: ''
141
+ - lib/file_processor.rb
142
+ - lib/openrouter_client.rb
143
+ homepage: https://github.com/pinkfloydsito/anki_generator
67
144
  licenses:
68
145
  - MIT
69
- metadata: {}
146
+ metadata:
147
+ homepage_uri: https://github.com/pinkfloydsito/anki_generator
148
+ source_code_uri: https://github.com/pinkfloydsito/anki_generator
149
+ changelog_uri: https://github.com/pinkfloydsito/anki_generator/blob/main/CHANGELOG.md
150
+ bug_tracker_uri: https://github.com/pinkfloydsito/anki_generator/issues
151
+ documentation_uri: https://github.com/pinkfloydsito/anki_generator/blob/main/README.md
70
152
  post_install_message:
71
153
  rdoc_options: []
72
154
  require_paths:
@@ -75,7 +157,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
75
157
  requirements:
76
158
  - - ">="
77
159
  - !ruby/object:Gem::Version
78
- version: '0'
160
+ version: 2.7.0
79
161
  required_rubygems_version: !ruby/object:Gem::Requirement
80
162
  requirements:
81
163
  - - ">="
@@ -85,5 +167,5 @@ requirements: []
85
167
  rubygems_version: 3.3.7
86
168
  signing_key:
87
169
  specification_version: 4
88
- summary: A tool to generate Anki .apkg files from YAML definitions.
170
+ summary: AI-powered Anki flashcard generator with file attachment support
89
171
  test_files: []