readwise-reader-cli 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lis186
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # Readwise Reader CLI
2
+
3
+ [![npm version](https://img.shields.io/npm/v/readwise-reader-cli.svg)](https://www.npmjs.com/package/readwise-reader-cli)
4
+ [![CI](https://github.com/lis186/readwise-reader-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/lis186/readwise-reader-cli/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A command-line interface for the [Readwise Reader](https://readwise.io/read) API.
8
+
9
+ Based on the official [Readwise Reader API](https://readwise.io/reader_api).
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install -g readwise-reader-cli
15
+ ```
16
+
17
+ Or run directly with npx:
18
+
19
+ ```bash
20
+ npx readwise-reader-cli list
21
+ npx readwise-reader-cli save https://example.com
22
+ ```
23
+
24
+ ## Setup
25
+
26
+ 1. Get your Readwise access token from [readwise.io/access_token](https://readwise.io/access_token)
27
+
28
+ 2. Set the token as an environment variable:
29
+
30
+ ```bash
31
+ export READWISE_TOKEN=your_token_here
32
+ ```
33
+
34
+ Or create a `.env` file in your project:
35
+
36
+ ```
37
+ READWISE_TOKEN=your_token_here
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### List documents
43
+
44
+ ```bash
45
+ # List all documents
46
+ reader list
47
+
48
+ # Filter by location
49
+ reader list --location later
50
+
51
+ # Filter by category
52
+ reader list --category article
53
+
54
+ # Limit results
55
+ reader list --limit 10
56
+
57
+ # Output as JSON
58
+ reader list --json
59
+ ```
60
+
61
+ ### Save a document
62
+
63
+ ```bash
64
+ # Save a URL
65
+ reader save https://example.com/article
66
+
67
+ # With metadata
68
+ reader save https://example.com/article \
69
+ --title "My Article" \
70
+ --author "John Doe" \
71
+ --tags "tech,programming" \
72
+ --location later
73
+
74
+ # Output as JSON
75
+ reader save https://example.com/article --json
76
+ ```
77
+
78
+ ### Update a document
79
+
80
+ ```bash
81
+ # Update title
82
+ reader update <document-id> --title "New Title"
83
+
84
+ # Update multiple fields
85
+ reader update <document-id> \
86
+ --title "New Title" \
87
+ --author "Jane Doe" \
88
+ --location archive
89
+
90
+ # Update tags
91
+ reader update <document-id> --tags "new,tags"
92
+ ```
93
+
94
+ ### Delete a document
95
+
96
+ ```bash
97
+ reader delete <document-id>
98
+ ```
99
+
100
+ ### List tags
101
+
102
+ ```bash
103
+ reader tags
104
+ reader tags --json
105
+ ```
106
+
107
+ ### Verify authentication
108
+
109
+ ```bash
110
+ reader auth
111
+ ```
112
+
113
+ ## Options
114
+
115
+ ### Global options
116
+
117
+ | Option | Description |
118
+ |--------|-------------|
119
+ | `--json` | Output results as JSON |
120
+ | `--help` | Show help |
121
+ | `--version` | Show version |
122
+
123
+ ### Location values
124
+
125
+ - `new` - Inbox
126
+ - `later` - Read Later
127
+ - `archive` - Archive
128
+ - `feed` - Feed
129
+
130
+ ### Category values
131
+
132
+ - `article`
133
+ - `email`
134
+ - `rss`
135
+ - `highlight`
136
+ - `note`
137
+ - `pdf`
138
+ - `epub`
139
+ - `tweet`
140
+ - `video`
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ # Clone the repository
146
+ git clone https://github.com/lis186/readwise-reader-cli.git
147
+ cd readwise-reader-cli
148
+
149
+ # Install dependencies
150
+ npm install
151
+
152
+ # Run in development mode
153
+ npm run dev -- list
154
+
155
+ # Run tests
156
+ npm test
157
+
158
+ # Build
159
+ npm run build
160
+ ```
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,12 @@
1
+ import type { ListDocumentsParams, ListDocumentsResponse, SaveDocumentParams, SaveDocumentResponse, UpdateDocumentParams, Document, ListTagsResponse } from './types.js';
2
+ export declare class ReadwiseApiClient {
3
+ private token;
4
+ constructor(token: string);
5
+ private request;
6
+ listDocuments(params?: ListDocumentsParams): Promise<ListDocumentsResponse>;
7
+ saveDocument(params: SaveDocumentParams): Promise<SaveDocumentResponse>;
8
+ updateDocument(id: string, params: UpdateDocumentParams): Promise<Document>;
9
+ deleteDocument(id: string): Promise<void>;
10
+ listTags(pageCursor?: string): Promise<ListTagsResponse>;
11
+ verifyAuth(): Promise<boolean>;
12
+ }
@@ -0,0 +1,79 @@
1
+ const API_BASE_URL = 'https://readwise.io/api/v3';
2
+ const AUTH_URL = 'https://readwise.io/api/v2/auth/';
3
+ export class ReadwiseApiClient {
4
+ token;
5
+ constructor(token) {
6
+ if (!token) {
7
+ throw new Error('Readwise API token is required');
8
+ }
9
+ this.token = token;
10
+ }
11
+ async request(endpoint, options = {}) {
12
+ const url = `${API_BASE_URL}${endpoint}`;
13
+ const headers = {
14
+ 'Authorization': `Token ${this.token}`,
15
+ 'Content-Type': 'application/json',
16
+ ...options.headers,
17
+ };
18
+ const response = await fetch(url, {
19
+ ...options,
20
+ headers,
21
+ });
22
+ if (!response.ok) {
23
+ if (response.status === 429) {
24
+ const retryAfter = response.headers.get('Retry-After');
25
+ throw new Error(`Rate limited. Retry after ${retryAfter} seconds.`);
26
+ }
27
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
28
+ }
29
+ // Handle 204 No Content
30
+ if (response.status === 204) {
31
+ return undefined;
32
+ }
33
+ return response.json();
34
+ }
35
+ async listDocuments(params = {}) {
36
+ const searchParams = new URLSearchParams();
37
+ Object.entries(params)
38
+ .filter(([_, v]) => v !== undefined)
39
+ .forEach(([k, v]) => searchParams.set(k, v === true ? 'true' : String(v)));
40
+ const query = searchParams.toString();
41
+ const endpoint = query ? `/list/?${query}` : '/list/';
42
+ return this.request(endpoint);
43
+ }
44
+ async saveDocument(params) {
45
+ return this.request('/save/', {
46
+ method: 'POST',
47
+ body: JSON.stringify(params),
48
+ });
49
+ }
50
+ async updateDocument(id, params) {
51
+ return this.request(`/update/${id}/`, {
52
+ method: 'PATCH',
53
+ body: JSON.stringify(params),
54
+ });
55
+ }
56
+ async deleteDocument(id) {
57
+ await this.request(`/delete/${id}/`, {
58
+ method: 'DELETE',
59
+ });
60
+ }
61
+ async listTags(pageCursor) {
62
+ const query = pageCursor ? `?pageCursor=${pageCursor}` : '';
63
+ return this.request(`/tags/${query}`);
64
+ }
65
+ async verifyAuth() {
66
+ const response = await fetch(AUTH_URL, {
67
+ headers: {
68
+ 'Authorization': `Token ${this.token}`,
69
+ },
70
+ });
71
+ if (response.status === 204) {
72
+ return true;
73
+ }
74
+ if (response.status === 401) {
75
+ return false;
76
+ }
77
+ throw new Error(`Auth check failed: ${response.status} ${response.statusText}`);
78
+ }
79
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReadwiseApiClient } from '../api-client.js';
2
+ import type { BaseCommandOptions } from '../types.js';
3
+ export type AuthCommandOptions = BaseCommandOptions;
4
+ export declare function verifyAuth(client: ReadwiseApiClient, options?: AuthCommandOptions): Promise<string>;
@@ -0,0 +1,10 @@
1
+ export async function verifyAuth(client, options = {}) {
2
+ const isValid = await client.verifyAuth();
3
+ if (options.json) {
4
+ return JSON.stringify({ valid: isValid }, null, 2);
5
+ }
6
+ if (isValid) {
7
+ return 'Token is valid.';
8
+ }
9
+ return 'Token is invalid.';
10
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReadwiseApiClient } from '../api-client.js';
2
+ import type { BaseCommandOptions } from '../types.js';
3
+ export type DeleteCommandOptions = BaseCommandOptions;
4
+ export declare function deleteDocument(client: ReadwiseApiClient, id: string, options?: DeleteCommandOptions): Promise<string>;
@@ -0,0 +1,7 @@
1
+ export async function deleteDocument(client, id, options = {}) {
2
+ await client.deleteDocument(id);
3
+ if (options.json) {
4
+ return JSON.stringify({ success: true, id }, null, 2);
5
+ }
6
+ return `Document ${id} deleted successfully.`;
7
+ }
@@ -0,0 +1,12 @@
1
+ export { listDocuments } from './list.js';
2
+ export type { ListCommandOptions } from './list.js';
3
+ export { saveDocument } from './save.js';
4
+ export type { SaveCommandOptions } from './save.js';
5
+ export { updateDocument } from './update.js';
6
+ export type { UpdateCommandOptions } from './update.js';
7
+ export { deleteDocument } from './delete.js';
8
+ export type { DeleteCommandOptions } from './delete.js';
9
+ export { listTags } from './tags.js';
10
+ export type { TagsCommandOptions } from './tags.js';
11
+ export { verifyAuth } from './auth.js';
12
+ export type { AuthCommandOptions } from './auth.js';
@@ -0,0 +1,6 @@
1
+ export { listDocuments } from './list.js';
2
+ export { saveDocument } from './save.js';
3
+ export { updateDocument } from './update.js';
4
+ export { deleteDocument } from './delete.js';
5
+ export { listTags } from './tags.js';
6
+ export { verifyAuth } from './auth.js';
@@ -0,0 +1,8 @@
1
+ import type { ReadwiseApiClient } from '../api-client.js';
2
+ import type { DocumentLocation, DocumentCategory, BaseCommandOptions } from '../types.js';
3
+ export interface ListCommandOptions extends BaseCommandOptions {
4
+ location?: DocumentLocation;
5
+ category?: DocumentCategory;
6
+ limit?: number;
7
+ }
8
+ export declare function listDocuments(client: ReadwiseApiClient, options?: ListCommandOptions): Promise<string>;
@@ -0,0 +1,29 @@
1
+ import { removeUndefined } from '../types.js';
2
+ export async function listDocuments(client, options = {}) {
3
+ const response = await client.listDocuments(removeUndefined({ location: options.location, category: options.category }));
4
+ const documents = options.limit
5
+ ? response.results.slice(0, options.limit)
6
+ : response.results;
7
+ if (options.json) {
8
+ return JSON.stringify(documents, null, 2);
9
+ }
10
+ if (documents.length === 0) {
11
+ return 'No documents found.';
12
+ }
13
+ const lines = documents.map((doc, index) => {
14
+ // Handle tags as either array or empty object from API
15
+ const tagsArray = Array.isArray(doc.tags) ? doc.tags : [];
16
+ const tags = tagsArray.map((t) => t.name).join(', ');
17
+ return [
18
+ `[${index + 1}] ${doc.title || '(Untitled)'}`,
19
+ ` ID: ${doc.id}`,
20
+ ` URL: ${doc.source_url || doc.url}`,
21
+ ` Location: ${doc.location} | Category: ${doc.category}`,
22
+ tags ? ` Tags: ${tags}` : null,
23
+ ` Saved: ${new Date(doc.saved_at).toLocaleDateString()}`,
24
+ ]
25
+ .filter(Boolean)
26
+ .join('\n');
27
+ });
28
+ return `Found ${response.count} documents:\n\n${lines.join('\n\n')}`;
29
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReadwiseApiClient } from '../api-client.js';
2
+ import type { DocumentLocation, DocumentCategory, BaseCommandOptions } from '../types.js';
3
+ export interface SaveCommandOptions extends BaseCommandOptions {
4
+ title?: string;
5
+ author?: string;
6
+ tags?: string[];
7
+ location?: DocumentLocation;
8
+ category?: DocumentCategory;
9
+ notes?: string;
10
+ }
11
+ export declare function saveDocument(client: ReadwiseApiClient, url: string, options?: SaveCommandOptions): Promise<string>;
@@ -0,0 +1,17 @@
1
+ import { removeUndefined } from '../types.js';
2
+ export async function saveDocument(client, url, options = {}) {
3
+ const params = removeUndefined({
4
+ url,
5
+ title: options.title,
6
+ author: options.author,
7
+ tags: options.tags,
8
+ location: options.location,
9
+ category: options.category,
10
+ notes: options.notes,
11
+ });
12
+ const response = await client.saveDocument(params);
13
+ if (options.json) {
14
+ return JSON.stringify(response, null, 2);
15
+ }
16
+ return `Document saved successfully!\n ID: ${response.id}\n URL: ${response.url}`;
17
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReadwiseApiClient } from '../api-client.js';
2
+ import type { BaseCommandOptions } from '../types.js';
3
+ export type TagsCommandOptions = BaseCommandOptions;
4
+ export declare function listTags(client: ReadwiseApiClient, options?: TagsCommandOptions): Promise<string>;
@@ -0,0 +1,11 @@
1
+ export async function listTags(client, options = {}) {
2
+ const response = await client.listTags();
3
+ if (options.json) {
4
+ return JSON.stringify(response.results, null, 2);
5
+ }
6
+ if (response.results.length === 0) {
7
+ return 'No tags found.';
8
+ }
9
+ const tagList = response.results.map((tag) => ` - ${tag.name}`).join('\n');
10
+ return `Tags (${response.count}):\n${tagList}`;
11
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReadwiseApiClient } from '../api-client.js';
2
+ import type { DocumentLocation, DocumentCategory, BaseCommandOptions } from '../types.js';
3
+ export interface UpdateCommandOptions extends BaseCommandOptions {
4
+ title?: string;
5
+ author?: string;
6
+ summary?: string;
7
+ location?: DocumentLocation;
8
+ category?: DocumentCategory;
9
+ tags?: string[];
10
+ }
11
+ export declare function updateDocument(client: ReadwiseApiClient, id: string, options?: UpdateCommandOptions): Promise<string>;
@@ -0,0 +1,13 @@
1
+ import { removeUndefined } from '../types.js';
2
+ export async function updateDocument(client, id, options = {}) {
3
+ const { json, ...params } = options;
4
+ const cleanParams = removeUndefined(params);
5
+ if (Object.keys(cleanParams).length === 0) {
6
+ throw new Error('At least one field must be provided to update');
7
+ }
8
+ const response = await client.updateDocument(id, cleanParams);
9
+ if (json) {
10
+ return JSON.stringify(response, null, 2);
11
+ }
12
+ return `Document updated successfully!\n ID: ${response.id}\n Title: ${response.title || '(Untitled)'}`;
13
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { config } from 'dotenv';
4
+ import { ReadwiseApiClient } from './api-client.js';
5
+ import { listDocuments, saveDocument, updateDocument, deleteDocument, listTags, verifyAuth, } from './commands/index.js';
6
+ // Load environment variables
7
+ config();
8
+ const program = new Command();
9
+ function getClient() {
10
+ const token = process.env.READWISE_TOKEN || process.env.READWISE_ACCESS_TOKEN;
11
+ if (!token) {
12
+ console.error('Error: READWISE_TOKEN or READWISE_ACCESS_TOKEN environment variable is required.');
13
+ console.error('Set it in your .env file or export it in your shell.');
14
+ process.exit(1);
15
+ }
16
+ return new ReadwiseApiClient(token);
17
+ }
18
+ // Common error handling wrapper for command actions
19
+ function withErrorHandling(handler) {
20
+ return async (...args) => {
21
+ try {
22
+ const client = getClient();
23
+ const result = await handler(client, ...args);
24
+ console.log(result);
25
+ }
26
+ catch (error) {
27
+ console.error('Error:', error.message);
28
+ process.exit(1);
29
+ }
30
+ };
31
+ }
32
+ program
33
+ .name('reader')
34
+ .description('CLI tool for Readwise Reader API')
35
+ .version('0.1.0');
36
+ // List documents command
37
+ program
38
+ .command('list')
39
+ .description('List documents from your Reader library')
40
+ .option('-l, --location <location>', 'Filter by location (new, later, archive, feed)')
41
+ .option('-c, --category <category>', 'Filter by category (article, email, pdf, etc.)')
42
+ .option('-n, --limit <number>', 'Limit number of results', parseInt)
43
+ .option('--json', 'Output as JSON')
44
+ .action(withErrorHandling(listDocuments));
45
+ // Save document command
46
+ program
47
+ .command('save <url>')
48
+ .description('Save a new document to Reader')
49
+ .option('-t, --title <title>', 'Document title')
50
+ .option('-a, --author <author>', 'Document author')
51
+ .option('--tags <tags>', 'Comma-separated tags', (val) => val.split(','))
52
+ .option('-l, --location <location>', 'Location (new, later, archive, feed)')
53
+ .option('-c, --category <category>', 'Category (article, email, pdf, etc.)')
54
+ .option('--notes <notes>', 'Notes to add')
55
+ .option('--json', 'Output as JSON')
56
+ .action(withErrorHandling(saveDocument));
57
+ // Update document command
58
+ program
59
+ .command('update <id>')
60
+ .description('Update an existing document')
61
+ .option('-t, --title <title>', 'New title')
62
+ .option('-a, --author <author>', 'New author')
63
+ .option('-s, --summary <summary>', 'New summary')
64
+ .option('-l, --location <location>', 'New location (new, later, archive, feed)')
65
+ .option('-c, --category <category>', 'New category')
66
+ .option('--tags <tags>', 'New tags (comma-separated)', (val) => val.split(','))
67
+ .option('--json', 'Output as JSON')
68
+ .action(withErrorHandling(updateDocument));
69
+ // Delete document command
70
+ program
71
+ .command('delete <id>')
72
+ .description('Delete a document')
73
+ .option('--json', 'Output as JSON')
74
+ .action(withErrorHandling(deleteDocument));
75
+ // List tags command
76
+ program
77
+ .command('tags')
78
+ .description('List all tags')
79
+ .option('--json', 'Output as JSON')
80
+ .action(withErrorHandling(listTags));
81
+ // Verify auth command
82
+ program
83
+ .command('auth')
84
+ .description('Verify API token is valid')
85
+ .option('--json', 'Output as JSON')
86
+ .action(withErrorHandling(verifyAuth));
87
+ program.parse();
@@ -0,0 +1,76 @@
1
+ export type DocumentLocation = 'new' | 'later' | 'archive' | 'feed';
2
+ export type DocumentCategory = 'article' | 'email' | 'rss' | 'highlight' | 'note' | 'pdf' | 'epub' | 'tweet' | 'video';
3
+ export interface Tag {
4
+ name: string;
5
+ }
6
+ export interface Document {
7
+ id: string;
8
+ url: string;
9
+ source_url: string;
10
+ title: string;
11
+ author: string | null;
12
+ source: string | null;
13
+ category: DocumentCategory;
14
+ location: DocumentLocation;
15
+ tags: Tag[] | Record<string, never>;
16
+ site_name: string | null;
17
+ word_count: number | null;
18
+ created_at: string;
19
+ updated_at: string;
20
+ published_date: string | null;
21
+ summary: string | null;
22
+ image_url: string | null;
23
+ reading_progress: number;
24
+ first_opened_at: string | null;
25
+ last_opened_at: string | null;
26
+ saved_at: string;
27
+ last_moved_at: string;
28
+ }
29
+ export interface ListDocumentsResponse {
30
+ count: number;
31
+ nextPageCursor: string | null;
32
+ results: Document[];
33
+ }
34
+ export interface ListDocumentsParams {
35
+ id?: string;
36
+ updatedAfter?: string;
37
+ location?: DocumentLocation;
38
+ category?: DocumentCategory;
39
+ pageCursor?: string;
40
+ withHtmlContent?: boolean;
41
+ }
42
+ export interface SaveDocumentParams {
43
+ url: string;
44
+ html?: string;
45
+ title?: string;
46
+ author?: string;
47
+ tags?: string[];
48
+ location?: DocumentLocation;
49
+ category?: DocumentCategory;
50
+ published_date?: string;
51
+ image_url?: string;
52
+ notes?: string;
53
+ }
54
+ export interface SaveDocumentResponse {
55
+ id: string;
56
+ url: string;
57
+ }
58
+ export interface UpdateDocumentParams {
59
+ title?: string;
60
+ author?: string;
61
+ summary?: string;
62
+ published_date?: string;
63
+ image_url?: string;
64
+ location?: DocumentLocation;
65
+ category?: DocumentCategory;
66
+ tags?: string[];
67
+ }
68
+ export interface ListTagsResponse {
69
+ count: number;
70
+ nextPageCursor: string | null;
71
+ results: Tag[];
72
+ }
73
+ export interface BaseCommandOptions {
74
+ json?: boolean;
75
+ }
76
+ export declare function removeUndefined<T extends object>(obj: T): T;
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ // Utility function to remove undefined values from an object
2
+ export function removeUndefined(obj) {
3
+ return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined));
4
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "readwise-reader-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for Readwise Reader API",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "reader": "./dist/index.js",
9
+ "readwise-reader-cli": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsx src/index.ts",
17
+ "test": "vitest",
18
+ "test:run": "vitest run",
19
+ "prepublishOnly": "npm run test:run && npm run build"
20
+ },
21
+ "keywords": [
22
+ "readwise",
23
+ "reader",
24
+ "cli",
25
+ "reading-list"
26
+ ],
27
+ "author": "lis186",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/lis186/readwise-reader-cli.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/lis186/readwise-reader-cli/issues"
35
+ },
36
+ "homepage": "https://github.com/lis186/readwise-reader-cli#readme",
37
+ "devDependencies": {
38
+ "@types/node": "^20.10.0",
39
+ "typescript": "^5.3.0",
40
+ "vitest": "^1.0.0",
41
+ "tsx": "^4.6.0"
42
+ },
43
+ "dependencies": {
44
+ "commander": "^12.0.0",
45
+ "dotenv": "^16.3.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ }
50
+ }