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 +21 -0
- package/README.md +164 -0
- package/dist/api-client.d.ts +12 -0
- package/dist/api-client.js +79 -0
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.js +10 -0
- package/dist/commands/delete.d.ts +4 -0
- package/dist/commands/delete.js +7 -0
- package/dist/commands/index.d.ts +12 -0
- package/dist/commands/index.js +6 -0
- package/dist/commands/list.d.ts +8 -0
- package/dist/commands/list.js +29 -0
- package/dist/commands/save.d.ts +11 -0
- package/dist/commands/save.js +17 -0
- package/dist/commands/tags.d.ts +4 -0
- package/dist/commands/tags.js +11 -0
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.js +13 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +87 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +4 -0
- package/package.json +50 -0
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
|
+
[](https://www.npmjs.com/package/readwise-reader-cli)
|
|
4
|
+
[](https://github.com/lis186/readwise-reader-cli/actions/workflows/ci.yml)
|
|
5
|
+
[](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,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,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
|
+
}
|
package/dist/index.d.ts
ADDED
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();
|
package/dist/types.d.ts
ADDED
|
@@ -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
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
|
+
}
|