Skip to content

Commit 2dc0467

Browse files
committed
Add CLI for creating documents
1 parent 03054b9 commit 2dc0467

File tree

7 files changed

+456
-1
lines changed

7 files changed

+456
-1
lines changed

README.md

Lines changed: 25 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,30 @@ 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 --file content.html
158+
159+
# Short form flag
160+
readwise document create -f content.html
161+
162+
# With options
163+
readwise document create --file content.html --title="My Article" --location=later
164+
165+
# See all available options
166+
readwise --help
167+
```
168+
145169
## Development
146170

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

0 commit comments

Comments
 (0)