Skip to content

Commit e3943e9

Browse files
andyw8joshbeckman
andauthored
Add CLI for creating documents (#16)
* Add CLI for creating documents Co-authored-by: Josh Beckman <[email protected]>
1 parent 230cbe2 commit e3943e9

File tree

7 files changed

+459
-1
lines changed

7 files changed

+459
-1
lines changed

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ puts document.child? # is this a highlight/note of another doc
101101

102102
# Check location
103103
puts document.in_new?
104-
puts document.in_later?
104+
puts document.in_later?
105105
puts document.in_archive?
106106

107107
# Check category
@@ -142,6 +142,32 @@ document = client.create_document(document: document_create)
142142
documents = client.create_documents(documents: [document_create1, document_create2])
143143
```
144144

145+
## Command Line Interface
146+
147+
This gem includes a `readwise` command-line tool for quickly sending HTML content to Readwise Reader.
148+
149+
First, set your API token:
150+
```bash
151+
export READWISE_API_KEY=your_token_here
152+
```
153+
154+
Then use the CLI to send HTML files:
155+
```bash
156+
# Basic usage
157+
readwise document create --html-file content.html
158+
readwise document create --url https://datatracker.ietf.org/doc/html/rfc2324
159+
160+
# Short form flag
161+
readwise document create -f content.html
162+
163+
# With options
164+
readwise document create --html-file content.html --title="My Article" --location=later
165+
166+
# See all available options
167+
readwise --help
168+
readwise document create --help
169+
```
170+
145171
## Development
146172

147173
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.

exe/readwise

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env ruby
2+
3+
require 'bundler/setup'
4+
require 'readwise'
5+
require 'readwise/cli'
6+
7+
Readwise::CLI.start

lib/readwise/cli.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
require_relative 'cli/base_command'
2+
require_relative 'cli/command_registry'
3+
4+
module Readwise
5+
class CLI
6+
def self.start(args = ARGV)
7+
new.start(args)
8+
end
9+
10+
def start(args)
11+
if args.empty?
12+
show_help
13+
exit 1
14+
end
15+
16+
if args.first == '--help' || args.first == '-h'
17+
show_help
18+
return
19+
end
20+
21+
resource = args.shift&.downcase
22+
action = args.shift&.downcase
23+
24+
unless resource && action
25+
puts "Error: Resource and action are required"
26+
puts "Usage: readwise <resource> <action> [options] [arguments]"
27+
puts "Run 'readwise --help' for more information"
28+
exit 1
29+
end
30+
31+
command_class = CommandRegistry.find(resource, action)
32+
unless command_class
33+
puts "Error: Unknown command '#{resource} #{action}'"
34+
puts "Run 'readwise --help' to see available commands"
35+
exit 1
36+
end
37+
38+
command = command_class.new
39+
command.execute(args)
40+
rescue => e
41+
puts "Unexpected error: #{e.message}"
42+
exit 1
43+
end
44+
45+
private
46+
47+
def show_help
48+
puts <<~HELP
49+
Usage: readwise <resource> <action> [options] [arguments]
50+
51+
Available commands:
52+
document create --html-file <file> Send HTML content to Readwise Reader
53+
54+
Global options:
55+
-h, --help Show this help message
56+
57+
Examples:
58+
readwise document create --html-file content.html --title="My Article"
59+
readwise document create -f content.html --title="My Article"
60+
readwise document create --help
61+
HELP
62+
end
63+
end
64+
end

lib/readwise/cli/base_command.rb

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
require 'optparse'
2+
3+
module Readwise
4+
class CLI
5+
class BaseCommand
6+
def initialize
7+
@options = {}
8+
end
9+
10+
def execute(args)
11+
parse_options(args)
12+
validate_arguments(args)
13+
run(args)
14+
rescue OptionParser::InvalidOption => e
15+
puts "Error: #{e.message}"
16+
show_help
17+
exit 1
18+
rescue ArgumentError => e
19+
puts "Error: #{e.message}"
20+
exit 1
21+
end
22+
23+
private
24+
25+
attr_reader :options
26+
27+
def parse_options(args)
28+
parser = create_option_parser
29+
parser.parse!(args)
30+
end
31+
32+
def create_option_parser
33+
OptionParser.new do |opts|
34+
opts.banner = banner
35+
opts.separator ""
36+
opts.separator description if description
37+
opts.separator ""
38+
opts.separator "Options:"
39+
40+
add_options(opts)
41+
42+
opts.on("-h", "--help", "Show this help message") do
43+
show_help
44+
exit
45+
end
46+
end
47+
end
48+
49+
def show_help
50+
puts create_option_parser.help
51+
end
52+
53+
def get_api_client
54+
token = ENV['READWISE_API_KEY']
55+
unless token
56+
puts "Error: READWISE_API_KEY environment variable is not set"
57+
exit 1
58+
end
59+
60+
Readwise::Client.new(token: token)
61+
end
62+
63+
def read_file(file_path)
64+
unless File.exist?(file_path)
65+
puts "Error: File '#{file_path}' not found"
66+
exit 1
67+
end
68+
69+
File.read(file_path)
70+
end
71+
72+
def handle_api_error(&block)
73+
yield
74+
rescue Readwise::Client::Error => e
75+
puts "API Error: #{e.message}"
76+
exit 1
77+
rescue => e
78+
puts "Unexpected error: #{e.message}"
79+
exit 1
80+
end
81+
82+
# Override these methods in subclasses
83+
84+
def banner
85+
"Usage: readwise"
86+
end
87+
88+
def description
89+
nil
90+
end
91+
92+
def add_options(opts)
93+
# Override in subclasses to add specific options
94+
end
95+
96+
def validate_arguments(args)
97+
# Override in subclasses to validate arguments
98+
end
99+
100+
def run(args)
101+
raise NotImplementedError, "Subclasses must implement #run"
102+
end
103+
end
104+
end
105+
end

lib/readwise/cli/command_registry.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module Readwise
2+
class CLI
3+
class CommandRegistry
4+
@commands = {}
5+
6+
def self.register(resource, action, command_class)
7+
key = "#{resource}:#{action}"
8+
@commands[key] = command_class
9+
end
10+
11+
def self.find(resource, action)
12+
key = "#{resource}:#{action}"
13+
@commands[key]
14+
end
15+
16+
def self.all_commands
17+
@commands.keys.map { |key| key.split(':') }
18+
end
19+
end
20+
end
21+
end
22+
23+
# Register available commands
24+
require_relative 'document/create_command'
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
require_relative '../base_command'
2+
3+
module Readwise
4+
class CLI
5+
module Document
6+
class CreateCommand < BaseCommand
7+
CATEGORIES = %w[article email rss highlight note pdf epub tweet video].freeze
8+
def banner
9+
"Usage: readwise document create [options]"
10+
end
11+
12+
def description
13+
"Sends HTML content to Readwise Reader API"
14+
end
15+
16+
def add_options(opts)
17+
opts.on("-f", "--html-file=FILE", "HTML file path") do |file|
18+
options[:file] = file
19+
end
20+
21+
opts.on("--title=TITLE", "Document title") do |title|
22+
options[:title] = title
23+
end
24+
25+
opts.on("--author=AUTHOR", "Document author") do |author|
26+
options[:author] = author
27+
end
28+
29+
opts.on("-u", "--url=URL", "Source URL (defaults to https://example.com/<filename>)") do |url|
30+
options[:url] = url
31+
end
32+
33+
opts.on("--summary=SUMMARY", "Document summary") do |summary|
34+
options[:summary] = summary
35+
end
36+
37+
opts.on("--notes=NOTES", "Personal notes") do |notes|
38+
options[:notes] = notes
39+
end
40+
41+
opts.on("--location=LOCATION", "Document location: new, later, archive, feed (default: new)") do |location|
42+
unless %w[new later archive feed].include?(location)
43+
puts "Error: Invalid location. Must be one of: new, later, archive, feed"
44+
exit 1
45+
end
46+
options[:location] = location
47+
end
48+
49+
opts.on("--category=CATEGORY", "Document category: #{CATEGORIES.join(', ')}") do |category|
50+
unless CATEGORIES.include?(category)
51+
puts "Error: Invalid category. Must be one of: #{CATEGORIES.join(', ')}"
52+
exit 1
53+
end
54+
options[:category] = category
55+
end
56+
57+
opts.on("--tags=TAGS", "Comma-separated list of tags") do |tags|
58+
options[:tags] = tags.split(',').map(&:strip)
59+
end
60+
61+
opts.on("--image-url=URL", "Image URL") do |image_url|
62+
options[:image_url] = image_url
63+
end
64+
65+
opts.on("--published-date=DATE", "Published date (ISO 8601 format)") do |date|
66+
options[:published_date] = date
67+
end
68+
69+
opts.on("--[no-]clean-html", "Clean HTML (default: true)") do |clean|
70+
options[:should_clean_html] = clean
71+
end
72+
73+
opts.on("--saved-using=SOURCE", "Saved using source (default: cli)") do |source|
74+
options[:saved_using] = source
75+
end
76+
end
77+
78+
def validate_arguments(args)
79+
unless options[:file] || options[:url]
80+
puts "Error: File path or URL is required"
81+
show_help
82+
exit 1
83+
end
84+
end
85+
86+
def run(args)
87+
html_file = options[:file]
88+
html_content = read_file(html_file) if html_file
89+
90+
document_params = build_document_params(html_content, html_file)
91+
92+
handle_api_error do
93+
client = get_api_client
94+
document_create = Readwise::DocumentCreate.new(**document_params)
95+
document = client.create_document(document: document_create)
96+
97+
puts "Document created successfully!"
98+
puts "ID: #{document.id}"
99+
puts "Title: #{document.title}"
100+
puts "Location: #{document.location}"
101+
puts "URL: #{document.url}" if document.url
102+
end
103+
end
104+
105+
private
106+
107+
def build_document_params(html_content, html_file)
108+
document_params = {
109+
html: html_content,
110+
location: options[:location] || 'new',
111+
should_clean_html: options.key?(:should_clean_html) ? options[:should_clean_html] : true,
112+
saved_using: options[:saved_using] || 'cli',
113+
url: options[:url] || "https://example.com/#{File.basename(html_file)}"
114+
}
115+
116+
[:title, :author, :summary, :notes, :category, :tags, :image_url, :published_date].each do |key|
117+
document_params[key] = options[key] if options[key]
118+
end
119+
120+
document_params
121+
end
122+
end
123+
end
124+
end
125+
end
126+
127+
# Register this command
128+
Readwise::CLI::CommandRegistry.register('document', 'create', Readwise::CLI::Document::CreateCommand)

0 commit comments

Comments
 (0)