Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ puts document.child? # is this a highlight/note of another doc

# Check location
puts document.in_new?
puts document.in_later?
puts document.in_later?
puts document.in_archive?

# Check category
Expand Down Expand Up @@ -142,6 +142,32 @@ document = client.create_document(document: document_create)
documents = client.create_documents(documents: [document_create1, document_create2])
```

## Command Line Interface

This gem includes a `readwise` command-line tool for quickly sending HTML content to Readwise Reader.

First, set your API token:
```bash
export READWISE_API_KEY=your_token_here
```

Then use the CLI to send HTML files:
```bash
# Basic usage
readwise document create --html-file content.html
readwise document create --url https://datatracker.ietf.org/doc/html/rfc2324

# Short form flag
readwise document create -f content.html

# With options
readwise document create --html-file content.html --title="My Article" --location=later

# See all available options
readwise --help
readwise document create --help
```

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
7 changes: 7 additions & 0 deletions exe/readwise
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env ruby

require 'bundler/setup'
require 'readwise'
require 'readwise/cli'

Readwise::CLI.start
64 changes: 64 additions & 0 deletions lib/readwise/cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require_relative 'cli/base_command'
require_relative 'cli/command_registry'

module Readwise
class CLI
def self.start(args = ARGV)
new.start(args)
end

def start(args)
if args.empty?
show_help
exit 1
end

if args.first == '--help' || args.first == '-h'
show_help
return
end

resource = args.shift&.downcase
action = args.shift&.downcase

unless resource && action
puts "Error: Resource and action are required"
puts "Usage: readwise <resource> <action> [options] [arguments]"
puts "Run 'readwise --help' for more information"
exit 1
end

command_class = CommandRegistry.find(resource, action)
unless command_class
puts "Error: Unknown command '#{resource} #{action}'"
puts "Run 'readwise --help' to see available commands"
exit 1
end

command = command_class.new
command.execute(args)
rescue => e
puts "Unexpected error: #{e.message}"
exit 1
end

private

def show_help
puts <<~HELP
Usage: readwise <resource> <action> [options] [arguments]

Available commands:
document create --html-file <file> Send HTML content to Readwise Reader

Global options:
-h, --help Show this help message

Examples:
readwise document create --html-file content.html --title="My Article"
readwise document create -f content.html --title="My Article"
readwise document create --help
HELP
end
end
end
105 changes: 105 additions & 0 deletions lib/readwise/cli/base_command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
require 'optparse'

module Readwise
class CLI
class BaseCommand
def initialize
@options = {}
end

def execute(args)
parse_options(args)
validate_arguments(args)
run(args)
rescue OptionParser::InvalidOption => e
puts "Error: #{e.message}"
show_help
exit 1
rescue ArgumentError => e
puts "Error: #{e.message}"
exit 1
end

private

attr_reader :options

def parse_options(args)
parser = create_option_parser
parser.parse!(args)
end

def create_option_parser
OptionParser.new do |opts|
opts.banner = banner
opts.separator ""
opts.separator description if description
opts.separator ""
opts.separator "Options:"

add_options(opts)

opts.on("-h", "--help", "Show this help message") do
show_help
exit
end
end
end

def show_help
puts create_option_parser.help
end

def get_api_client
token = ENV['READWISE_API_KEY']
unless token
puts "Error: READWISE_API_KEY environment variable is not set"
exit 1
end

Readwise::Client.new(token: token)
end

def read_file(file_path)
unless File.exist?(file_path)
puts "Error: File '#{file_path}' not found"
exit 1
end

File.read(file_path)
end

def handle_api_error(&block)
yield
rescue Readwise::Client::Error => e
puts "API Error: #{e.message}"
exit 1
rescue => e
puts "Unexpected error: #{e.message}"
exit 1
end

# Override these methods in subclasses

def banner
"Usage: readwise"
end

def description
nil
end

def add_options(opts)
# Override in subclasses to add specific options
end

def validate_arguments(args)
# Override in subclasses to validate arguments
end

def run(args)
raise NotImplementedError, "Subclasses must implement #run"
end
end
end
end
24 changes: 24 additions & 0 deletions lib/readwise/cli/command_registry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Readwise
class CLI
class CommandRegistry
@commands = {}

def self.register(resource, action, command_class)
key = "#{resource}:#{action}"
@commands[key] = command_class
end

def self.find(resource, action)
key = "#{resource}:#{action}"
@commands[key]
end

def self.all_commands
@commands.keys.map { |key| key.split(':') }
end
end
end
end

# Register available commands
require_relative 'document/create_command'
128 changes: 128 additions & 0 deletions lib/readwise/cli/document/create_command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
require_relative '../base_command'

module Readwise
class CLI
module Document
class CreateCommand < BaseCommand
CATEGORIES = %w[article email rss highlight note pdf epub tweet video].freeze
def banner
"Usage: readwise document create [options]"
end

def description
"Sends HTML content to Readwise Reader API"
end

def add_options(opts)
opts.on("-f", "--html-file=FILE", "HTML file path") do |file|
options[:file] = file
end

opts.on("--title=TITLE", "Document title") do |title|
options[:title] = title
end

opts.on("--author=AUTHOR", "Document author") do |author|
options[:author] = author
end

opts.on("-u", "--url=URL", "Source URL (defaults to https://example.com/<filename>)") do |url|
options[:url] = url
end

opts.on("--summary=SUMMARY", "Document summary") do |summary|
options[:summary] = summary
end

opts.on("--notes=NOTES", "Personal notes") do |notes|
options[:notes] = notes
end

opts.on("--location=LOCATION", "Document location: new, later, archive, feed (default: new)") do |location|
unless %w[new later archive feed].include?(location)
puts "Error: Invalid location. Must be one of: new, later, archive, feed"
exit 1
end
options[:location] = location
end

opts.on("--category=CATEGORY", "Document category: #{CATEGORIES.join(', ')}") do |category|
unless CATEGORIES.include?(category)
puts "Error: Invalid category. Must be one of: #{CATEGORIES.join(', ')}"
exit 1
end
options[:category] = category
end

opts.on("--tags=TAGS", "Comma-separated list of tags") do |tags|
options[:tags] = tags.split(',').map(&:strip)
end

opts.on("--image-url=URL", "Image URL") do |image_url|
options[:image_url] = image_url
end

opts.on("--published-date=DATE", "Published date (ISO 8601 format)") do |date|
options[:published_date] = date
end

opts.on("--[no-]clean-html", "Clean HTML (default: true)") do |clean|
options[:should_clean_html] = clean
end

opts.on("--saved-using=SOURCE", "Saved using source (default: cli)") do |source|
options[:saved_using] = source
end
end

def validate_arguments(args)
unless options[:file] || options[:url]
puts "Error: File path or URL is required"
show_help
exit 1
end
end

def run(args)
html_file = options[:file]
html_content = read_file(html_file) if html_file

document_params = build_document_params(html_content, html_file)

handle_api_error do
client = get_api_client
document_create = Readwise::DocumentCreate.new(**document_params)
document = client.create_document(document: document_create)

puts "Document created successfully!"
puts "ID: #{document.id}"
puts "Title: #{document.title}"
puts "Location: #{document.location}"
puts "URL: #{document.url}" if document.url
end
end

private

def build_document_params(html_content, html_file)
document_params = {
html: html_content,
location: options[:location] || 'new',
should_clean_html: options.key?(:should_clean_html) ? options[:should_clean_html] : true,
saved_using: options[:saved_using] || 'cli',
url: options[:url] || "https://example.com/#{File.basename(html_file)}"
}

[:title, :author, :summary, :notes, :category, :tags, :image_url, :published_date].each do |key|
document_params[key] = options[key] if options[key]
end

document_params
end
end
end
end
end

# Register this command
Readwise::CLI::CommandRegistry.register('document', 'create', Readwise::CLI::Document::CreateCommand)
Loading