@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/src/config.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface ToolDef {
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: string;
|
|
10
|
+
properties?: Record<string, SchemaProperty>;
|
|
11
|
+
required?: string[];
|
|
12
|
+
$defs?: Record<string, SchemaProperty>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SchemaProperty {
|
|
17
|
+
type?: string;
|
|
18
|
+
format?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
enum?: string[];
|
|
21
|
+
items?: SchemaProperty;
|
|
22
|
+
default?: unknown;
|
|
23
|
+
anyOf?: SchemaProperty[];
|
|
24
|
+
$ref?: string;
|
|
25
|
+
properties?: Record<string, SchemaProperty>;
|
|
26
|
+
required?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Config {
|
|
30
|
+
client_id?: string;
|
|
31
|
+
client_secret?: string;
|
|
32
|
+
access_token?: string;
|
|
33
|
+
refresh_token?: string;
|
|
34
|
+
expires_at?: number;
|
|
35
|
+
auth_type?: "oauth" | "token";
|
|
36
|
+
tools_cache?: {
|
|
37
|
+
tools: ToolDef[];
|
|
38
|
+
fetched_at: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
43
|
+
|
|
44
|
+
export function getConfigPath(): string {
|
|
45
|
+
return join(homedir(), ".readwise-cli.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function loadConfig(): Promise<Config> {
|
|
49
|
+
try {
|
|
50
|
+
const data = await readFile(getConfigPath(), "utf-8");
|
|
51
|
+
return JSON.parse(data) as Config;
|
|
52
|
+
} catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function saveConfig(config: Config): Promise<void> {
|
|
58
|
+
await writeFile(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isCacheValid(config: Config): boolean {
|
|
62
|
+
if (!config.tools_cache) return false;
|
|
63
|
+
return Date.now() - config.tools_cache.fetched_at < CACHE_TTL_MS;
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
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 } from "./mcp.js";
|
|
6
|
+
import { registerTools } from "./commands.js";
|
|
7
|
+
import { loadConfig } from "./config.js";
|
|
8
|
+
import { VERSION } from "./version.js";
|
|
9
|
+
|
|
10
|
+
function readHiddenInput(prompt: string): Promise<string> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
if (!process.stdin.isTTY) {
|
|
13
|
+
// Piped input (e.g. echo $TOKEN | readwise login-with-token)
|
|
14
|
+
const rl = createInterface({ input: process.stdin });
|
|
15
|
+
rl.once("line", (line) => { resolve(line.trim()); rl.close(); });
|
|
16
|
+
rl.once("close", () => resolve(""));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
process.stdout.write(prompt);
|
|
21
|
+
process.stdin.setRawMode(true);
|
|
22
|
+
process.stdin.resume();
|
|
23
|
+
process.stdin.setEncoding("utf-8");
|
|
24
|
+
|
|
25
|
+
let input = "";
|
|
26
|
+
const onData = (ch: string) => {
|
|
27
|
+
if (ch === "\r" || ch === "\n" || ch === "\u0004") {
|
|
28
|
+
process.stdin.removeListener("data", onData);
|
|
29
|
+
process.stdin.setRawMode(false);
|
|
30
|
+
process.stdin.pause();
|
|
31
|
+
process.stdout.write("\n");
|
|
32
|
+
resolve(input);
|
|
33
|
+
} else if (ch === "\u0003") {
|
|
34
|
+
process.stdin.removeListener("data", onData);
|
|
35
|
+
process.stdin.setRawMode(false);
|
|
36
|
+
process.stdin.pause();
|
|
37
|
+
process.stdout.write("\n");
|
|
38
|
+
reject(new Error("Aborted"));
|
|
39
|
+
} else if (ch === "\u007f" || ch === "\b") {
|
|
40
|
+
input = input.slice(0, -1);
|
|
41
|
+
} else {
|
|
42
|
+
input += ch;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
process.stdin.on("data", onData);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const program = new Command();
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.name("readwise")
|
|
53
|
+
.version(VERSION)
|
|
54
|
+
.description("Command-line interface for Readwise and Reader")
|
|
55
|
+
.option("--json", "Output raw JSON (machine-readable)")
|
|
56
|
+
.option("--refresh", "Force-refresh the tool cache");
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command("login")
|
|
60
|
+
.description("Authenticate with Readwise via OAuth (opens browser)")
|
|
61
|
+
.action(async () => {
|
|
62
|
+
try {
|
|
63
|
+
await login();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
process.stderr.write(`\x1b[31m${(err as Error).message}\x1b[0m\n`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.command("login-with-token [token]")
|
|
72
|
+
.description("Authenticate with a Readwise access token (for scripts/CI)")
|
|
73
|
+
.action(async (token?: string) => {
|
|
74
|
+
try {
|
|
75
|
+
if (!token) {
|
|
76
|
+
console.log("Get your token from https://readwise.io/access_token");
|
|
77
|
+
token = await readHiddenInput("Enter token: ");
|
|
78
|
+
if (!token) {
|
|
79
|
+
process.stderr.write("\x1b[31mNo token provided.\x1b[0m\n");
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await loginWithToken(token);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
process.stderr.write(`\x1b[31m${(err as Error).message}\x1b[0m\n`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
const config = await loadConfig();
|
|
93
|
+
const forceRefresh = process.argv.includes("--refresh");
|
|
94
|
+
const positionalArgs = process.argv.slice(2).filter((a) => !a.startsWith("--"));
|
|
95
|
+
const hasSubcommand = positionalArgs.length > 0;
|
|
96
|
+
|
|
97
|
+
// If no subcommand, TTY, and authenticated → launch TUI
|
|
98
|
+
if (!hasSubcommand && process.stdout.isTTY && config.access_token) {
|
|
99
|
+
try {
|
|
100
|
+
const { token, authType } = await ensureValidToken();
|
|
101
|
+
const tools = await getTools(token, authType, forceRefresh);
|
|
102
|
+
const { startTui } = await import("./tui/index.js");
|
|
103
|
+
await startTui(tools, token, authType);
|
|
104
|
+
return;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
process.stderr.write(`\x1b[33mWarning: Could not start TUI: ${(err as Error).message}\x1b[0m\n`);
|
|
107
|
+
// Fall through to Commander help
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If no subcommand and not authenticated → hint to login
|
|
112
|
+
if (!hasSubcommand && process.stdout.isTTY && !config.access_token) {
|
|
113
|
+
await program.parseAsync(process.argv);
|
|
114
|
+
console.log("\nRun `readwise login` or `readwise login-with-token` to authenticate.");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Try to load tools if we have a token (for subcommand mode)
|
|
119
|
+
if (config.access_token) {
|
|
120
|
+
try {
|
|
121
|
+
const { token, authType } = await ensureValidToken();
|
|
122
|
+
const tools = await getTools(token, authType, forceRefresh);
|
|
123
|
+
registerTools(program, tools);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// Don't fail — login command should still work
|
|
126
|
+
// Only warn if user is trying to run a non-login command
|
|
127
|
+
if (hasSubcommand && positionalArgs[0] !== "login" && positionalArgs[0] !== "login-with-token") {
|
|
128
|
+
process.stderr.write(`\x1b[33mWarning: Could not fetch tools: ${(err as Error).message}\x1b[0m\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await program.parseAsync(process.argv);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main();
|
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { loadConfig, saveConfig, isCacheValid, type ToolDef } from "./config.js";
|
|
4
|
+
import { VERSION } from "./version.js";
|
|
5
|
+
|
|
6
|
+
const MCP_URL = "https://mcp2.readwise.io/mcp";
|
|
7
|
+
|
|
8
|
+
function createTransport(token: string, authType: "oauth" | "token"): StreamableHTTPClientTransport {
|
|
9
|
+
const authHeader = authType === "token" ? `Token ${token}` : `Bearer ${token}`;
|
|
10
|
+
return new StreamableHTTPClientTransport(new URL(MCP_URL), {
|
|
11
|
+
requestInit: {
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: authHeader,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getTools(token: string, authType: "oauth" | "token", forceRefresh = false): Promise<ToolDef[]> {
|
|
20
|
+
if (!forceRefresh) {
|
|
21
|
+
const config = await loadConfig();
|
|
22
|
+
if (isCacheValid(config)) {
|
|
23
|
+
return config.tools_cache!.tools;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const client = new Client({ name: "readwise", version: VERSION });
|
|
28
|
+
const transport = createTransport(token, authType);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await client.connect(transport);
|
|
32
|
+
const result = await client.listTools();
|
|
33
|
+
|
|
34
|
+
const tools = result.tools as ToolDef[];
|
|
35
|
+
|
|
36
|
+
// Cache
|
|
37
|
+
const config = await loadConfig();
|
|
38
|
+
config.tools_cache = {
|
|
39
|
+
tools,
|
|
40
|
+
fetched_at: Date.now(),
|
|
41
|
+
};
|
|
42
|
+
await saveConfig(config);
|
|
43
|
+
|
|
44
|
+
return tools;
|
|
45
|
+
} finally {
|
|
46
|
+
await client.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function callTool(
|
|
51
|
+
token: string,
|
|
52
|
+
authType: "oauth" | "token",
|
|
53
|
+
name: string,
|
|
54
|
+
args: Record<string, unknown>,
|
|
55
|
+
): Promise<{ content: Array<{ type: string; text?: string }>; structuredContent?: Record<string, unknown>; isError?: boolean }> {
|
|
56
|
+
const client = new Client({ name: "readwise", version: VERSION });
|
|
57
|
+
const transport = createTransport(token, authType);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await client.connect(transport);
|
|
61
|
+
const result = await client.callTool({ name, arguments: args });
|
|
62
|
+
return result as { content: Array<{ type: string; text?: string }>; structuredContent?: Record<string, unknown>; isError?: boolean };
|
|
63
|
+
} finally {
|
|
64
|
+
await client.close();
|
|
65
|
+
}
|
|
66
|
+
}
|