@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/dist/index.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline";
3
+ import { Command } from "commander";
4
+ import { login, loginWithToken, ensureValidToken } from "./auth.js";
5
+ import { getTools, callTool } from "./mcp.js";
6
+ import { registerTools, displayResult } from "./commands.js";
7
+ import { loadConfig } from "./config.js";
8
+ import { VERSION } from "./version.js";
9
+ function readHiddenInput(prompt) {
10
+ return new Promise((resolve, reject) => {
11
+ if (!process.stdin.isTTY) {
12
+ // Piped input (e.g. echo $TOKEN | readwise login-with-token)
13
+ const rl = createInterface({ input: process.stdin });
14
+ rl.once("line", (line) => { resolve(line.trim()); rl.close(); });
15
+ rl.once("close", () => resolve(""));
16
+ return;
17
+ }
18
+ process.stdout.write(prompt);
19
+ process.stdin.setRawMode(true);
20
+ process.stdin.resume();
21
+ process.stdin.setEncoding("utf-8");
22
+ let input = "";
23
+ const onData = (ch) => {
24
+ if (ch === "\r" || ch === "\n" || ch === "\u0004") {
25
+ process.stdin.removeListener("data", onData);
26
+ process.stdin.setRawMode(false);
27
+ process.stdin.pause();
28
+ process.stdout.write("\n");
29
+ resolve(input);
30
+ }
31
+ else if (ch === "\u0003") {
32
+ process.stdin.removeListener("data", onData);
33
+ process.stdin.setRawMode(false);
34
+ process.stdin.pause();
35
+ process.stdout.write("\n");
36
+ reject(new Error("Aborted"));
37
+ }
38
+ else if (ch === "\u007f" || ch === "\b") {
39
+ input = input.slice(0, -1);
40
+ }
41
+ else {
42
+ input += ch;
43
+ }
44
+ };
45
+ process.stdin.on("data", onData);
46
+ });
47
+ }
48
+ const program = new Command();
49
+ program
50
+ .name("readwise")
51
+ .version(VERSION)
52
+ .description("Command-line interface for Readwise and Reader")
53
+ .option("--json", "Output raw JSON (machine-readable)")
54
+ .option("--refresh", "Force-refresh the tool cache");
55
+ program
56
+ .command("login")
57
+ .description("Authenticate with Readwise via OAuth (opens browser)")
58
+ .action(async () => {
59
+ try {
60
+ await login();
61
+ }
62
+ catch (err) {
63
+ process.stderr.write(`\x1b[31m${err.message}\x1b[0m\n`);
64
+ process.exitCode = 1;
65
+ }
66
+ });
67
+ program
68
+ .command("login-with-token [token]")
69
+ .description("Authenticate with a Readwise access token (for scripts/CI)")
70
+ .action(async (token) => {
71
+ try {
72
+ if (!token) {
73
+ console.log("Get your token from https://readwise.io/access_token");
74
+ token = await readHiddenInput("Enter token: ");
75
+ if (!token) {
76
+ process.stderr.write("\x1b[31mNo token provided.\x1b[0m\n");
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ }
81
+ await loginWithToken(token);
82
+ }
83
+ catch (err) {
84
+ process.stderr.write(`\x1b[31m${err.message}\x1b[0m\n`);
85
+ process.exitCode = 1;
86
+ }
87
+ });
88
+ program
89
+ .command("search <query>")
90
+ .description("Search across Reader documents and Readwise highlights")
91
+ .option("--limit <n>", "Max results per source (default: 10)")
92
+ .action(async (query, options) => {
93
+ try {
94
+ const { token, authType } = await ensureValidToken();
95
+ const limit = options.limit ? Number(options.limit) : 10;
96
+ const json = program.opts().json || false;
97
+ const [readerResult, highlightsResult] = await Promise.all([
98
+ callTool(token, authType, "reader_search_documents", { query, limit }),
99
+ callTool(token, authType, "readwise_search_highlights", { vector_search_term: query, limit }),
100
+ ]);
101
+ if (json) {
102
+ const combined = {};
103
+ for (const item of readerResult.content) {
104
+ if (item.type === "text" && item.text) {
105
+ try {
106
+ combined.reader = JSON.parse(item.text);
107
+ }
108
+ catch {
109
+ combined.reader = item.text;
110
+ }
111
+ }
112
+ }
113
+ for (const item of highlightsResult.content) {
114
+ if (item.type === "text" && item.text) {
115
+ try {
116
+ combined.highlights = JSON.parse(item.text);
117
+ }
118
+ catch {
119
+ combined.highlights = item.text;
120
+ }
121
+ }
122
+ }
123
+ process.stdout.write(JSON.stringify(combined) + "\n");
124
+ }
125
+ else {
126
+ console.log("\x1b[1m\x1b[36m── Reader Documents ──\x1b[0m\n");
127
+ displayResult(readerResult, false);
128
+ console.log("\n\x1b[1m\x1b[36m── Readwise Highlights ──\x1b[0m\n");
129
+ displayResult(highlightsResult, false);
130
+ }
131
+ }
132
+ catch (err) {
133
+ process.stderr.write(`\x1b[31m${err.message}\x1b[0m\n`);
134
+ process.exitCode = 1;
135
+ }
136
+ });
137
+ async function main() {
138
+ const config = await loadConfig();
139
+ const forceRefresh = process.argv.includes("--refresh");
140
+ const positionalArgs = process.argv.slice(2).filter((a) => !a.startsWith("--"));
141
+ const hasSubcommand = positionalArgs.length > 0;
142
+ // If no subcommand, TTY, and authenticated → launch TUI
143
+ if (!hasSubcommand && process.stdout.isTTY && config.access_token) {
144
+ try {
145
+ const { token, authType } = await ensureValidToken();
146
+ const tools = await getTools(token, authType, forceRefresh);
147
+ const { startTui } = await import("./tui/index.js");
148
+ await startTui(tools, token, authType);
149
+ return;
150
+ }
151
+ catch (err) {
152
+ process.stderr.write(`\x1b[33mWarning: Could not start TUI: ${err.message}\x1b[0m\n`);
153
+ // Fall through to Commander help
154
+ }
155
+ }
156
+ // If no subcommand and not authenticated → hint to login
157
+ if (!hasSubcommand && process.stdout.isTTY && !config.access_token) {
158
+ await program.parseAsync(process.argv);
159
+ console.log("\nRun `readwise login` or `readwise login-with-token` to authenticate.");
160
+ return;
161
+ }
162
+ // Try to load tools if we have a token (for subcommand mode)
163
+ if (config.access_token) {
164
+ try {
165
+ const { token, authType } = await ensureValidToken();
166
+ const tools = await getTools(token, authType, forceRefresh);
167
+ registerTools(program, tools);
168
+ }
169
+ catch (err) {
170
+ // Don't fail — login command should still work
171
+ // Only warn if user is trying to run a non-login command
172
+ if (hasSubcommand && positionalArgs[0] !== "login" && positionalArgs[0] !== "login-with-token") {
173
+ process.stderr.write(`\x1b[33mWarning: Could not fetch tools: ${err.message}\x1b[0m\n`);
174
+ }
175
+ }
176
+ }
177
+ await program.parseAsync(process.argv);
178
+ }
179
+ main();
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { type ToolDef } from "./config.js";
2
+ export declare function getTools(token: string, authType: "oauth" | "token", forceRefresh?: boolean): Promise<ToolDef[]>;
3
+ export declare function callTool(token: string, authType: "oauth" | "token", name: string, args: Record<string, unknown>): Promise<{
4
+ content: Array<{
5
+ type: string;
6
+ text?: string;
7
+ }>;
8
+ structuredContent?: Record<string, unknown>;
9
+ isError?: boolean;
10
+ }>;
package/dist/mcp.js ADDED
@@ -0,0 +1,53 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { loadConfig, saveConfig, isCacheValid } from "./config.js";
4
+ import { VERSION } from "./version.js";
5
+ const MCP_URL = "https://mcp2.readwise.io/mcp";
6
+ function createTransport(token, authType) {
7
+ const authHeader = authType === "token" ? `Token ${token}` : `Bearer ${token}`;
8
+ return new StreamableHTTPClientTransport(new URL(MCP_URL), {
9
+ requestInit: {
10
+ headers: {
11
+ Authorization: authHeader,
12
+ },
13
+ },
14
+ });
15
+ }
16
+ export async function getTools(token, authType, forceRefresh = false) {
17
+ if (!forceRefresh) {
18
+ const config = await loadConfig();
19
+ if (isCacheValid(config)) {
20
+ return config.tools_cache.tools;
21
+ }
22
+ }
23
+ const client = new Client({ name: "readwise", version: VERSION });
24
+ const transport = createTransport(token, authType);
25
+ try {
26
+ await client.connect(transport);
27
+ const result = await client.listTools();
28
+ const tools = result.tools;
29
+ // Cache
30
+ const config = await loadConfig();
31
+ config.tools_cache = {
32
+ tools,
33
+ fetched_at: Date.now(),
34
+ };
35
+ await saveConfig(config);
36
+ return tools;
37
+ }
38
+ finally {
39
+ await client.close();
40
+ }
41
+ }
42
+ export async function callTool(token, authType, name, args) {
43
+ const client = new Client({ name: "readwise", version: VERSION });
44
+ const transport = createTransport(token, authType);
45
+ try {
46
+ await client.connect(transport);
47
+ const result = await client.callTool({ name, arguments: args });
48
+ return result;
49
+ }
50
+ finally {
51
+ await client.close();
52
+ }
53
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDef } from "../config.js";
2
+ export declare function runApp(tools: ToolDef[]): Promise<void>;