@readwise/cli 0.3.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/README.md +140 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +194 -0
- package/dist/commands.d.ts +14 -0
- package/dist/commands.js +152 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +24 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +179 -0
- package/dist/mcp.d.ts +10 -0
- package/dist/mcp.js +53 -0
- package/dist/tui/app.d.ts +2 -0
- package/dist/tui/app.js +1806 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.js +11 -0
- package/dist/tui/logo.d.ts +1 -0
- package/dist/tui/logo.js +16 -0
- package/dist/tui/term.d.ts +32 -0
- package/dist/tui/term.js +147 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +30 -0
- package/src/auth.ts +248 -0
- package/src/commands.ts +158 -0
- package/src/config.ts +64 -0
- package/src/index.ts +136 -0
- package/src/mcp.ts +66 -0
- package/src/tui/app.ts +1917 -0
- package/src/tui/index.ts +12 -0
- package/src/tui/logo.ts +16 -0
- package/src/tui/term.ts +151 -0
- package/src/version.ts +4 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# readwise
|
|
2
|
+
|
|
3
|
+
A command-line interface for [Readwise](https://readwise.io) and [Reader](https://read.readwise.io). Search your highlights, manage your reading list, tag and organize documents — all from the terminal.
|
|
4
|
+
|
|
5
|
+
Commands are auto-discovered from the Readwise API, so the CLI stays up to date as new features are added.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone <repo-url> && cd readwise
|
|
11
|
+
npm install
|
|
12
|
+
npm run build
|
|
13
|
+
npm link
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
### Interactive login (opens browser)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
readwise login
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Access token login (for scripts/CI)
|
|
25
|
+
|
|
26
|
+
Get your token from [readwise.io/access_token](https://readwise.io/access_token), then:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
readwise login-with-token
|
|
30
|
+
# prompts for token (hidden input, not stored in shell history)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You can also pipe the token in:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
echo "$READWISE_TOKEN" | readwise login-with-token
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Credentials are stored in `~/.readwise-cli.json`. OAuth tokens refresh automatically.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
Run `readwise --help` to see all available commands, or `readwise <command> --help` for details on a specific command.
|
|
44
|
+
|
|
45
|
+
### Search documents
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
readwise reader-search-documents --query "machine learning"
|
|
49
|
+
readwise reader-search-documents --query "react" --category-in article
|
|
50
|
+
readwise reader-search-documents --query "notes" --location-in shortlist --limit 5
|
|
51
|
+
readwise reader-search-documents --query "physics" --published-date-gt 2024-01-01
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Search highlights
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
readwise readwise-search-highlights --vector-search-term "spaced repetition"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### List and inspect documents
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
readwise reader-list-documents --limit 5
|
|
64
|
+
readwise reader-list-documents --category article --location later
|
|
65
|
+
readwise reader-list-documents --tag "to-review"
|
|
66
|
+
readwise reader-get-document-details --document-id <document-id>
|
|
67
|
+
readwise reader-get-document-highlights --document-id <document-id>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Save a document
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
readwise reader-create-document --url "https://example.com/article"
|
|
74
|
+
readwise reader-create-document \
|
|
75
|
+
--url "https://example.com" \
|
|
76
|
+
--title "My Article" \
|
|
77
|
+
--tags "reading-list,research" \
|
|
78
|
+
--notes "Found via HN"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Organize
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Tags
|
|
85
|
+
readwise reader-list-tags
|
|
86
|
+
readwise reader-add-tags-to-document --document-id <id> --tag-names "important,review"
|
|
87
|
+
readwise reader-remove-tags-from-document --document-id <id> --tag-names "old-tag"
|
|
88
|
+
|
|
89
|
+
# Move between locations (new/later/shortlist/archive)
|
|
90
|
+
readwise reader-move-document --document-id <id> --location archive
|
|
91
|
+
|
|
92
|
+
# Edit metadata
|
|
93
|
+
readwise reader-edit-document-metadata --document-id <id> --title "Better Title"
|
|
94
|
+
readwise reader-set-document-notes --document-id <id> --notes "Updated notes"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Highlight management
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
readwise reader-add-tags-to-highlight --document-id <id> --highlight-document-id <id> --tag-names "key-insight"
|
|
101
|
+
readwise reader-remove-tags-from-highlight --document-id <id> --highlight-document-id <id> --tag-names "old-tag"
|
|
102
|
+
readwise reader-set-highlight-notes --document-id <id> --highlight-document-id <id> --notes "This connects to..."
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Export
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
readwise reader-export-documents
|
|
109
|
+
readwise reader-export-documents --since-updated "2024-06-01T00:00:00Z"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Options
|
|
113
|
+
|
|
114
|
+
| Flag | Description |
|
|
115
|
+
|------|-------------|
|
|
116
|
+
| `--json` | Output raw JSON (for piping to `jq`, scripts, etc.) |
|
|
117
|
+
| `--refresh` | Force-refresh the command list from the server |
|
|
118
|
+
| `--help` | Show all commands or command-specific options |
|
|
119
|
+
|
|
120
|
+
## Examples
|
|
121
|
+
|
|
122
|
+
Pipe results to `jq`:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
readwise reader-list-documents --limit 3 --json | jq '.results[].title'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Run without building
|
|
132
|
+
npx tsx src/index.ts --help
|
|
133
|
+
|
|
134
|
+
# Build
|
|
135
|
+
npm run build
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## How it works
|
|
139
|
+
|
|
140
|
+
The CLI connects to the [Readwise MCP server](https://mcp2.readwise.io) internally, auto-discovers available tools, and exposes each one as a CLI command. The tool list is cached locally for 24 hours.
|
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import open from "open";
|
|
5
|
+
import { loadConfig, saveConfig } from "./config.js";
|
|
6
|
+
const DISCOVERY_URL = "https://readwise.io/o/.well-known/oauth-authorization-server";
|
|
7
|
+
const REDIRECT_URI = "http://localhost:6274/callback";
|
|
8
|
+
const SCOPES = "openid read write";
|
|
9
|
+
async function discover() {
|
|
10
|
+
const res = await fetch(DISCOVERY_URL);
|
|
11
|
+
if (!res.ok)
|
|
12
|
+
throw new Error(`OAuth discovery failed: ${res.status} ${res.statusText}`);
|
|
13
|
+
return (await res.json());
|
|
14
|
+
}
|
|
15
|
+
async function registerClient(registrationEndpoint) {
|
|
16
|
+
const res = await fetch(registrationEndpoint, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
client_name: "readwise-cli",
|
|
21
|
+
redirect_uris: [REDIRECT_URI],
|
|
22
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
23
|
+
token_endpoint_auth_method: "client_secret_basic",
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const body = await res.text();
|
|
28
|
+
throw new Error(`Client registration failed: ${res.status} ${body}`);
|
|
29
|
+
}
|
|
30
|
+
return (await res.json());
|
|
31
|
+
}
|
|
32
|
+
function generatePKCE() {
|
|
33
|
+
const verifier = randomBytes(48).toString("base64url");
|
|
34
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
35
|
+
return { verifier, challenge };
|
|
36
|
+
}
|
|
37
|
+
function waitForCallback(state) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
let timeout;
|
|
40
|
+
const cleanup = () => {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
server.close();
|
|
43
|
+
};
|
|
44
|
+
const server = createServer((req, res) => {
|
|
45
|
+
const url = new URL(req.url, `http://localhost:6274`);
|
|
46
|
+
if (url.pathname !== "/callback") {
|
|
47
|
+
res.writeHead(404);
|
|
48
|
+
res.end();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const code = url.searchParams.get("code");
|
|
52
|
+
const returnedState = url.searchParams.get("state");
|
|
53
|
+
const error = url.searchParams.get("error");
|
|
54
|
+
if (error) {
|
|
55
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
56
|
+
res.end(`<html><body><h1>Login failed</h1><p>${error}</p><p>You can close this tab.</p></body></html>`);
|
|
57
|
+
cleanup();
|
|
58
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!code || returnedState !== state) {
|
|
62
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
63
|
+
res.end(`<html><body><h1>Invalid callback</h1><p>You can close this tab.</p></body></html>`);
|
|
64
|
+
cleanup();
|
|
65
|
+
reject(new Error("Invalid callback: missing code or state mismatch"));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
69
|
+
res.end(`<html><body><h1>Login successful!</h1><p>You can close this tab and return to the terminal.</p></body></html>`);
|
|
70
|
+
cleanup();
|
|
71
|
+
resolve(code);
|
|
72
|
+
});
|
|
73
|
+
server.listen(6274, () => {
|
|
74
|
+
// Server ready
|
|
75
|
+
});
|
|
76
|
+
server.on("error", (err) => {
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
reject(new Error(`Failed to start callback server: ${err.message}`));
|
|
79
|
+
});
|
|
80
|
+
// Timeout after 2 minutes
|
|
81
|
+
timeout = setTimeout(() => {
|
|
82
|
+
server.close();
|
|
83
|
+
reject(new Error("Login timed out — no callback received within 2 minutes"));
|
|
84
|
+
}, 120_000);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async function exchangeToken(tokenEndpoint, code, clientId, clientSecret, codeVerifier) {
|
|
88
|
+
const res = await fetch(tokenEndpoint, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
92
|
+
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
|
|
93
|
+
},
|
|
94
|
+
body: new URLSearchParams({
|
|
95
|
+
grant_type: "authorization_code",
|
|
96
|
+
code,
|
|
97
|
+
redirect_uri: REDIRECT_URI,
|
|
98
|
+
code_verifier: codeVerifier,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const body = await res.text();
|
|
103
|
+
throw new Error(`Token exchange failed: ${res.status} ${body}`);
|
|
104
|
+
}
|
|
105
|
+
return (await res.json());
|
|
106
|
+
}
|
|
107
|
+
export async function login() {
|
|
108
|
+
console.log("Discovering OAuth endpoints...");
|
|
109
|
+
const metadata = await discover();
|
|
110
|
+
let config = await loadConfig();
|
|
111
|
+
// Register client if needed
|
|
112
|
+
if (!config.client_id || !config.client_secret) {
|
|
113
|
+
console.log("Registering client...");
|
|
114
|
+
const { client_id, client_secret } = await registerClient(metadata.registration_endpoint);
|
|
115
|
+
config.client_id = client_id;
|
|
116
|
+
config.client_secret = client_secret;
|
|
117
|
+
await saveConfig(config);
|
|
118
|
+
}
|
|
119
|
+
const { verifier, challenge } = generatePKCE();
|
|
120
|
+
const state = randomBytes(16).toString("hex");
|
|
121
|
+
const authUrl = new URL(metadata.authorization_endpoint);
|
|
122
|
+
authUrl.searchParams.set("response_type", "code");
|
|
123
|
+
authUrl.searchParams.set("client_id", config.client_id);
|
|
124
|
+
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
125
|
+
authUrl.searchParams.set("scope", SCOPES);
|
|
126
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
127
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
128
|
+
authUrl.searchParams.set("state", state);
|
|
129
|
+
// Start callback server before opening browser
|
|
130
|
+
const codePromise = waitForCallback(state);
|
|
131
|
+
console.log("Opening browser for authentication...");
|
|
132
|
+
await open(authUrl.toString());
|
|
133
|
+
const code = await codePromise;
|
|
134
|
+
console.log("Exchanging authorization code for tokens...");
|
|
135
|
+
const tokens = await exchangeToken(metadata.token_endpoint, code, config.client_id, config.client_secret, verifier);
|
|
136
|
+
config.access_token = tokens.access_token;
|
|
137
|
+
config.refresh_token = tokens.refresh_token;
|
|
138
|
+
config.expires_at = Date.now() + tokens.expires_in * 1000;
|
|
139
|
+
config.auth_type = "oauth";
|
|
140
|
+
await saveConfig(config);
|
|
141
|
+
console.log("Login successful! Tokens saved to ~/.readwise-cli.json");
|
|
142
|
+
}
|
|
143
|
+
export async function loginWithToken(token) {
|
|
144
|
+
const config = await loadConfig();
|
|
145
|
+
config.access_token = token;
|
|
146
|
+
config.auth_type = "token";
|
|
147
|
+
delete config.refresh_token;
|
|
148
|
+
delete config.expires_at;
|
|
149
|
+
delete config.client_id;
|
|
150
|
+
delete config.client_secret;
|
|
151
|
+
await saveConfig(config);
|
|
152
|
+
console.log("Token saved to ~/.readwise-cli.json");
|
|
153
|
+
}
|
|
154
|
+
export async function ensureValidToken() {
|
|
155
|
+
const config = await loadConfig();
|
|
156
|
+
if (!config.access_token) {
|
|
157
|
+
throw new Error("Not logged in. Run `readwise-cli login` or `readwise-cli login-with-token <token>` first.");
|
|
158
|
+
}
|
|
159
|
+
const authType = config.auth_type ?? "oauth";
|
|
160
|
+
// Access tokens don't expire and don't need refresh
|
|
161
|
+
if (authType === "token") {
|
|
162
|
+
return { token: config.access_token, authType };
|
|
163
|
+
}
|
|
164
|
+
// Refresh if expired or expiring within 60s
|
|
165
|
+
if (config.expires_at && Date.now() > config.expires_at - 60_000) {
|
|
166
|
+
if (!config.refresh_token || !config.client_id || !config.client_secret) {
|
|
167
|
+
throw new Error("Cannot refresh token — missing credentials. Run `readwise-cli login` again.");
|
|
168
|
+
}
|
|
169
|
+
console.error("Refreshing access token...");
|
|
170
|
+
const metadata = await discover();
|
|
171
|
+
const res = await fetch(metadata.token_endpoint, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
175
|
+
Authorization: `Basic ${Buffer.from(`${config.client_id}:${config.client_secret}`).toString("base64")}`,
|
|
176
|
+
},
|
|
177
|
+
body: new URLSearchParams({
|
|
178
|
+
grant_type: "refresh_token",
|
|
179
|
+
refresh_token: config.refresh_token,
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
if (!res.ok) {
|
|
183
|
+
const body = await res.text();
|
|
184
|
+
throw new Error(`Token refresh failed: ${res.status} ${body}. Run \`readwise-cli login\` again.`);
|
|
185
|
+
}
|
|
186
|
+
const tokens = (await res.json());
|
|
187
|
+
config.access_token = tokens.access_token;
|
|
188
|
+
if (tokens.refresh_token)
|
|
189
|
+
config.refresh_token = tokens.refresh_token;
|
|
190
|
+
config.expires_at = Date.now() + tokens.expires_in * 1000;
|
|
191
|
+
await saveConfig(config);
|
|
192
|
+
}
|
|
193
|
+
return { token: config.access_token, authType };
|
|
194
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { ToolDef, SchemaProperty } from "./config.js";
|
|
3
|
+
export declare function toolNameToCommand(name: string): string;
|
|
4
|
+
export declare function resolveRef(prop: SchemaProperty, defs?: Record<string, SchemaProperty>): SchemaProperty;
|
|
5
|
+
export declare function resolveProperty(prop: SchemaProperty, defs?: Record<string, SchemaProperty>): SchemaProperty;
|
|
6
|
+
export declare function displayResult(result: {
|
|
7
|
+
content: Array<{
|
|
8
|
+
type: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
}>;
|
|
11
|
+
structuredContent?: Record<string, unknown>;
|
|
12
|
+
isError?: boolean;
|
|
13
|
+
}, json: boolean): void;
|
|
14
|
+
export declare function registerTools(program: Command, tools: ToolDef[]): void;
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ensureValidToken } from "./auth.js";
|
|
2
|
+
import { callTool } from "./mcp.js";
|
|
3
|
+
export function toolNameToCommand(name) {
|
|
4
|
+
return name.replace(/_/g, "-");
|
|
5
|
+
}
|
|
6
|
+
export function resolveRef(prop, defs) {
|
|
7
|
+
if (prop.$ref && defs) {
|
|
8
|
+
const name = prop.$ref.replace("#/$defs/", "");
|
|
9
|
+
const resolved = defs[name];
|
|
10
|
+
if (resolved)
|
|
11
|
+
return { ...resolved, description: prop.description || resolved.description };
|
|
12
|
+
}
|
|
13
|
+
return prop;
|
|
14
|
+
}
|
|
15
|
+
export function resolveProperty(prop, defs) {
|
|
16
|
+
let p = resolveRef(prop, defs);
|
|
17
|
+
if (p.anyOf) {
|
|
18
|
+
const nonNull = p.anyOf.find((v) => v.type !== "null");
|
|
19
|
+
if (nonNull) {
|
|
20
|
+
p = { ...p, ...resolveRef(nonNull, defs), anyOf: undefined };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Resolve items.$ref for array types
|
|
24
|
+
if (p.type === "array" && p.items) {
|
|
25
|
+
p = { ...p, items: resolveRef(p.items, defs) };
|
|
26
|
+
// Also resolve anyOf inside items (e.g. items wrapped in anyOf)
|
|
27
|
+
if (p.items?.anyOf) {
|
|
28
|
+
const nonNull = p.items.anyOf.find((v) => v.type !== "null");
|
|
29
|
+
if (nonNull) {
|
|
30
|
+
const resolvedItem = resolveRef(nonNull, defs);
|
|
31
|
+
p = { ...p, items: { ...p.items, ...resolvedItem, anyOf: undefined } };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return p;
|
|
36
|
+
}
|
|
37
|
+
function optionFlag(name, prop) {
|
|
38
|
+
const flag = `--${name.replace(/_/g, "-")}`;
|
|
39
|
+
if (prop.type === "boolean") {
|
|
40
|
+
return flag;
|
|
41
|
+
}
|
|
42
|
+
return `${flag} <value>`;
|
|
43
|
+
}
|
|
44
|
+
function parseValue(value, prop) {
|
|
45
|
+
if (prop.type === "integer" || prop.type === "number") {
|
|
46
|
+
const n = Number(value);
|
|
47
|
+
if (isNaN(n))
|
|
48
|
+
throw new Error(`Expected a number for value: ${value}`);
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
if (prop.type === "array") {
|
|
52
|
+
// Try JSON first, then comma-separated
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(value);
|
|
55
|
+
if (Array.isArray(parsed))
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// fall through
|
|
60
|
+
}
|
|
61
|
+
return value.split(",").map((s) => s.trim());
|
|
62
|
+
}
|
|
63
|
+
if (prop.type === "boolean") {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
export function displayResult(result, json) {
|
|
69
|
+
if (result.isError) {
|
|
70
|
+
for (const item of result.content) {
|
|
71
|
+
if (item.text) {
|
|
72
|
+
process.stderr.write(`\x1b[31mError: ${item.text}\x1b[0m\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Prefer text content; fall back to structuredContent for empty results
|
|
79
|
+
let printed = false;
|
|
80
|
+
for (const item of result.content) {
|
|
81
|
+
if (item.type === "text" && item.text) {
|
|
82
|
+
printed = true;
|
|
83
|
+
if (json) {
|
|
84
|
+
process.stdout.write(item.text + "\n");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(item.text);
|
|
89
|
+
console.log(JSON.stringify(parsed, null, 2));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
console.log(item.text);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!printed && result.structuredContent) {
|
|
98
|
+
const data = result.structuredContent;
|
|
99
|
+
if (json) {
|
|
100
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.log(JSON.stringify(data, null, 2));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function registerTools(program, tools) {
|
|
108
|
+
for (const tool of tools) {
|
|
109
|
+
const cmd = program
|
|
110
|
+
.command(toolNameToCommand(tool.name))
|
|
111
|
+
.description(tool.description || "");
|
|
112
|
+
const properties = tool.inputSchema.properties || {};
|
|
113
|
+
const required = new Set(tool.inputSchema.required || []);
|
|
114
|
+
const defs = tool.inputSchema.$defs;
|
|
115
|
+
for (const [propName, rawProp] of Object.entries(properties)) {
|
|
116
|
+
const prop = resolveProperty(rawProp, defs);
|
|
117
|
+
const flag = optionFlag(propName, prop);
|
|
118
|
+
const parts = [];
|
|
119
|
+
if (prop.description)
|
|
120
|
+
parts.push(prop.description);
|
|
121
|
+
if (required.has(propName))
|
|
122
|
+
parts.push("(required)");
|
|
123
|
+
const enumValues = prop.enum || prop.items?.enum;
|
|
124
|
+
if (enumValues)
|
|
125
|
+
parts.push(`[${enumValues.join(", ")}]`);
|
|
126
|
+
if (prop.default !== undefined)
|
|
127
|
+
parts.push(`(default: ${JSON.stringify(prop.default)})`);
|
|
128
|
+
cmd.option(flag, parts.join(" ") || undefined);
|
|
129
|
+
}
|
|
130
|
+
cmd.action(async (options) => {
|
|
131
|
+
try {
|
|
132
|
+
const { token, authType } = await ensureValidToken();
|
|
133
|
+
// Convert commander options back to tool arguments
|
|
134
|
+
const args = {};
|
|
135
|
+
for (const [propName, rawProp] of Object.entries(properties)) {
|
|
136
|
+
const prop = resolveProperty(rawProp, defs);
|
|
137
|
+
const camelKey = propName.replace(/_/g, "-").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
138
|
+
const value = options[camelKey];
|
|
139
|
+
if (value !== undefined) {
|
|
140
|
+
args[propName] = parseValue(String(value), prop);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const result = await callTool(token, authType, tool.name, args);
|
|
144
|
+
displayResult(result, program.opts().json || false);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
process.stderr.write(`\x1b[31m${err.message}\x1b[0m\n`);
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface ToolDef {
|
|
2
|
+
name: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties?: Record<string, SchemaProperty>;
|
|
7
|
+
required?: string[];
|
|
8
|
+
$defs?: Record<string, SchemaProperty>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface SchemaProperty {
|
|
12
|
+
type?: string;
|
|
13
|
+
format?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
enum?: string[];
|
|
16
|
+
items?: SchemaProperty;
|
|
17
|
+
default?: unknown;
|
|
18
|
+
anyOf?: SchemaProperty[];
|
|
19
|
+
$ref?: string;
|
|
20
|
+
properties?: Record<string, SchemaProperty>;
|
|
21
|
+
required?: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface Config {
|
|
24
|
+
client_id?: string;
|
|
25
|
+
client_secret?: string;
|
|
26
|
+
access_token?: string;
|
|
27
|
+
refresh_token?: string;
|
|
28
|
+
expires_at?: number;
|
|
29
|
+
auth_type?: "oauth" | "token";
|
|
30
|
+
tools_cache?: {
|
|
31
|
+
tools: ToolDef[];
|
|
32
|
+
fetched_at: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export declare function getConfigPath(): string;
|
|
36
|
+
export declare function loadConfig(): Promise<Config>;
|
|
37
|
+
export declare function saveConfig(config: Config): Promise<void>;
|
|
38
|
+
export declare function isCacheValid(config: Config): boolean;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
5
|
+
export function getConfigPath() {
|
|
6
|
+
return join(homedir(), ".readwise-cli.json");
|
|
7
|
+
}
|
|
8
|
+
export async function loadConfig() {
|
|
9
|
+
try {
|
|
10
|
+
const data = await readFile(getConfigPath(), "utf-8");
|
|
11
|
+
return JSON.parse(data);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function saveConfig(config) {
|
|
18
|
+
await writeFile(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
19
|
+
}
|
|
20
|
+
export function isCacheValid(config) {
|
|
21
|
+
if (!config.tools_cache)
|
|
22
|
+
return false;
|
|
23
|
+
return Date.now() - config.tools_cache.fetched_at < CACHE_TTL_MS;
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED