@vakra-dev/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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/dist/index.js +297 -0
  4. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vakra-dev
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,177 @@
1
+ # Reader CLI
2
+
3
+ Read the web from your terminal. Built for AI coding agents.
4
+
5
+ The Reader CLI scrapes, crawls, and screenshots web pages via the [Reader API](https://reader.dev). It outputs clean markdown to stdout, ready to pipe into any AI workflow.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @vakra-dev/reader-cli
11
+ ```
12
+
13
+ Or run directly:
14
+
15
+ ```bash
16
+ npx @vakra-dev/reader-cli scrape https://example.com
17
+ ```
18
+
19
+ ## Setup
20
+
21
+ **For humans** - save your API key once:
22
+
23
+ ```bash
24
+ reader config set api-key rdr_your_key_here
25
+ ```
26
+
27
+ **For AI agents / CI** - set the environment variable:
28
+
29
+ ```bash
30
+ export READER_API_KEY=rdr_your_key_here
31
+ ```
32
+
33
+ Get your API key at [console.reader.dev](https://console.reader.dev).
34
+
35
+ ## Usage
36
+
37
+ ### Scrape a page
38
+
39
+ ```bash
40
+ reader scrape https://example.com
41
+ ```
42
+
43
+ Output is clean markdown, printed to stdout:
44
+
45
+ ```bash
46
+ reader scrape https://docs.stripe.com/payments > stripe-payments.md
47
+ ```
48
+
49
+ ### Formats
50
+
51
+ ```bash
52
+ reader scrape https://example.com # markdown (default)
53
+ reader scrape https://example.com -f html # cleaned HTML
54
+ reader scrape https://example.com -f screenshot -o page.png # full-page screenshot
55
+ ```
56
+
57
+ ### JSON output
58
+
59
+ Get the full API response with metadata:
60
+
61
+ ```bash
62
+ reader scrape https://example.com --json
63
+ ```
64
+
65
+ ### Crawl a site
66
+
67
+ Discover and scrape all pages:
68
+
69
+ ```bash
70
+ reader crawl https://docs.example.com --max-pages 20
71
+ ```
72
+
73
+ Save pages to individual files:
74
+
75
+ ```bash
76
+ reader crawl https://docs.example.com -o ./docs/
77
+ ```
78
+
79
+ List URLs only:
80
+
81
+ ```bash
82
+ reader crawl https://example.com --urls-only
83
+ ```
84
+
85
+ ### Check status
86
+
87
+ ```bash
88
+ reader status
89
+ ```
90
+
91
+ ```
92
+ Reader CLI v0.1.0
93
+ API: https://api.reader.dev (connected)
94
+ Key: rdr_...c3bb
95
+ Credits: 874 / 1000 (free tier)
96
+ Resets: 2026-07-01
97
+ ```
98
+
99
+ ### Check credits
100
+
101
+ ```bash
102
+ reader credits
103
+ reader credits --json
104
+ ```
105
+
106
+ ### Configuration
107
+
108
+ ```bash
109
+ reader config set api-key <key> # save API key
110
+ reader config set api-url <url> # custom API URL
111
+ reader config show # show current config
112
+ ```
113
+
114
+ Config is stored at `~/.reader/config.json`. Environment variables take precedence:
115
+
116
+ | Setting | Env var | Default |
117
+ |---------|---------|---------|
118
+ | API key | `READER_API_KEY` | - |
119
+ | API URL | `READER_API_URL` | `https://api.reader.dev` |
120
+
121
+ ## Command reference
122
+
123
+ ### `reader scrape <url>`
124
+
125
+ | Flag | Description | Default |
126
+ |------|-------------|---------|
127
+ | `-f, --format` | Output format: `markdown`, `html`, `screenshot` | `markdown` |
128
+ | `--json` | Full JSON response | - |
129
+ | `-o, --output` | Write to file | stdout |
130
+ | `--no-main-content` | Include nav, header, footer | - |
131
+ | `--include-tags` | CSS selectors to keep | - |
132
+ | `--exclude-tags` | CSS selectors to remove | - |
133
+ | `--wait-for` | Wait for CSS selector | - |
134
+ | `--timeout` | Timeout in ms | `30000` |
135
+ | `--proxy-mode` | `standard`, `stealth`, `auto` | `auto` |
136
+
137
+ ### `reader crawl <url>`
138
+
139
+ | Flag | Description | Default |
140
+ |------|-------------|---------|
141
+ | `--max-depth` | Crawl depth | `2` |
142
+ | `--max-pages` | Max pages to crawl | `20` |
143
+ | `--urls-only` | Only list URLs | - |
144
+ | `--json` | Full JSON response | - |
145
+ | `-o, --output-dir` | Write pages to directory | stdout |
146
+
147
+ ### `reader status`
148
+
149
+ Shows CLI version, API connectivity, credit balance, and tier.
150
+
151
+ ### `reader credits`
152
+
153
+ | Flag | Description |
154
+ |------|-------------|
155
+ | `--json` | Full JSON response |
156
+
157
+ ### `reader config`
158
+
159
+ | Subcommand | Description |
160
+ |------------|-------------|
161
+ | `set api-key <key>` | Save API key |
162
+ | `set api-url <url>` | Set custom API URL |
163
+ | `show` | Display current config |
164
+
165
+ ## For AI agents
166
+
167
+ The CLI is designed for seamless use with AI coding agents like Claude Code, Cursor, and Codex:
168
+
169
+ - **Markdown to stdout** - agents read content directly
170
+ - **Errors to stderr** - never pollutes piped output
171
+ - **Exit codes** - 0 on success, 1 on error
172
+ - **No interactive prompts** - everything via flags and env vars
173
+ - **`--json` flag** - structured output for programmatic use
174
+
175
+ ## License
176
+
177
+ MIT - see [LICENSE](LICENSE).
package/dist/index.js ADDED
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/scrape.ts
7
+ import { ReaderClient } from "@vakra-dev/reader-js";
8
+
9
+ // src/utils/config.ts
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
11
+ import { join } from "path";
12
+ import { homedir } from "os";
13
+ var CONFIG_DIR = join(homedir(), ".reader");
14
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
15
+ function loadConfig() {
16
+ try {
17
+ if (existsSync(CONFIG_FILE)) {
18
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
19
+ }
20
+ } catch {
21
+ }
22
+ return {};
23
+ }
24
+ function saveConfig(config) {
25
+ mkdirSync(CONFIG_DIR, { recursive: true });
26
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
27
+ }
28
+ function getApiKey() {
29
+ if (process.env.READER_API_KEY) {
30
+ return process.env.READER_API_KEY;
31
+ }
32
+ const config = loadConfig();
33
+ if (config.apiKey) {
34
+ return config.apiKey;
35
+ }
36
+ console.error("Error: No API key configured.\n");
37
+ console.error(" For humans: reader config set api-key <your-key>");
38
+ console.error(" For agents: export READER_API_KEY=<your-key>");
39
+ console.error("\n Get your key at https://console.reader.dev");
40
+ process.exit(1);
41
+ }
42
+ function getApiUrl() {
43
+ return process.env.READER_API_URL || loadConfig().apiUrl || "https://api.reader.dev";
44
+ }
45
+ function redactKey(key) {
46
+ if (key.length <= 8) return "****";
47
+ return key.slice(0, 4) + "..." + key.slice(-4);
48
+ }
49
+
50
+ // src/utils/output.ts
51
+ import { writeFileSync as writeFileSync2 } from "fs";
52
+ function info(msg) {
53
+ console.error(msg);
54
+ }
55
+ function error(msg) {
56
+ console.error(`Error: ${msg}`);
57
+ }
58
+ function outputContent(content) {
59
+ process.stdout.write(content);
60
+ }
61
+ function outputJson(data) {
62
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
63
+ }
64
+ function saveScreenshot(base64, outputPath) {
65
+ const buffer = Buffer.from(base64, "base64");
66
+ const path = outputPath || "screenshot.png";
67
+ writeFileSync2(path, buffer);
68
+ return path;
69
+ }
70
+
71
+ // src/commands/scrape.ts
72
+ function registerScrapeCommand(program2) {
73
+ program2.command("scrape <url>").description("Scrape a URL and output content").option("-f, --format <format>", "Output format: markdown (default), html, screenshot", "markdown").option("--json", "Output full JSON response").option("-o, --output <file>", "Write output to file").option("--no-main-content", "Include full page (nav, header, footer)").option("--include-tags <selectors>", "CSS selectors to include (comma-separated)").option("--exclude-tags <selectors>", "CSS selectors to exclude (comma-separated)").option("--wait-for <selector>", "Wait for CSS selector before scraping").option("--timeout <ms>", "Timeout in milliseconds", "30000").option("--proxy-mode <mode>", "Proxy mode: standard, stealth, auto").action(async (url, opts) => {
74
+ const apiKey = getApiKey();
75
+ const client = new ReaderClient({ apiKey, baseUrl: getApiUrl() });
76
+ const formats = [];
77
+ const requestedFormat = opts.format;
78
+ if (requestedFormat === "screenshot") {
79
+ formats.push("screenshot");
80
+ } else if (requestedFormat === "html") {
81
+ formats.push("html");
82
+ } else {
83
+ formats.push("markdown");
84
+ }
85
+ if (requestedFormat !== "screenshot" && opts.output?.endsWith(".png")) {
86
+ formats.push("screenshot");
87
+ }
88
+ try {
89
+ const result = await client.read({
90
+ url,
91
+ formats,
92
+ onlyMainContent: opts.mainContent !== false,
93
+ includeTags: opts.includeTags?.split(",").map((s) => s.trim()),
94
+ excludeTags: opts.excludeTags?.split(",").map((s) => s.trim()),
95
+ waitForSelector: opts.waitFor,
96
+ timeoutMs: parseInt(opts.timeout, 10),
97
+ proxyMode: opts.proxyMode
98
+ });
99
+ if (result.kind === "scrape") {
100
+ const data = result.data;
101
+ if (opts.json) {
102
+ outputJson(data);
103
+ return;
104
+ }
105
+ if (data.screenshot) {
106
+ const path = saveScreenshot(data.screenshot, opts.output);
107
+ info(`Screenshot saved to ${path}`);
108
+ if (requestedFormat === "screenshot") return;
109
+ }
110
+ const content = data.markdown || data.html || "";
111
+ if (opts.output && !opts.output.endsWith(".png")) {
112
+ const { writeFileSync: writeFileSync3 } = await import("fs");
113
+ writeFileSync3(opts.output, content);
114
+ info(`Written to ${opts.output}`);
115
+ } else {
116
+ outputContent(content);
117
+ }
118
+ } else {
119
+ const job = result.data;
120
+ if (opts.json) {
121
+ outputJson(job);
122
+ } else {
123
+ for (const page of job.results) {
124
+ outputContent(page.markdown || page.html || "");
125
+ }
126
+ }
127
+ }
128
+ } catch (err) {
129
+ const msg = err instanceof Error ? err.message : String(err);
130
+ error(msg);
131
+ process.exit(1);
132
+ }
133
+ });
134
+ }
135
+
136
+ // src/commands/crawl.ts
137
+ import { ReaderClient as ReaderClient2 } from "@vakra-dev/reader-js";
138
+ function registerCrawlCommand(program2) {
139
+ program2.command("crawl <url>").description("Crawl a website and output discovered pages").option("--max-depth <n>", "Maximum crawl depth", "2").option("--max-pages <n>", "Maximum pages to crawl", "20").option("--urls-only", "Only output discovered URLs, don't scrape content").option("--json", "Output full JSON response").option("-o, --output-dir <dir>", "Write each page to a separate file").action(async (url, opts) => {
140
+ const apiKey = getApiKey();
141
+ const client = new ReaderClient2({ apiKey, baseUrl: getApiUrl() });
142
+ info(`Crawling ${url} (depth: ${opts.maxDepth}, max: ${opts.maxPages} pages)...`);
143
+ try {
144
+ const result = await client.read({
145
+ url,
146
+ maxDepth: parseInt(opts.maxDepth, 10),
147
+ maxPages: parseInt(opts.maxPages, 10),
148
+ formats: opts.urlsOnly ? [] : ["markdown"]
149
+ });
150
+ if (result.kind !== "job") {
151
+ error("Unexpected response - expected a crawl job");
152
+ process.exit(1);
153
+ }
154
+ const job = result.data;
155
+ if (opts.json) {
156
+ outputJson(job);
157
+ return;
158
+ }
159
+ if (opts.urlsOnly || job.results.length === 0) {
160
+ for (const page of job.results) {
161
+ outputContent(page.url + "\n");
162
+ }
163
+ info(`
164
+ ${job.results.length} URLs discovered`);
165
+ return;
166
+ }
167
+ if (opts.outputDir) {
168
+ const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync3 } = await import("fs");
169
+ mkdirSync2(opts.outputDir, { recursive: true });
170
+ for (const page of job.results) {
171
+ const slug = new URL(page.url).pathname.replace(/\//g, "_").replace(/^_/, "").replace(/_$/, "") || "index";
172
+ const filename = `${opts.outputDir}/${slug}.md`;
173
+ writeFileSync3(filename, page.markdown || page.html || "");
174
+ }
175
+ info(`${job.results.length} pages written to ${opts.outputDir}/`);
176
+ } else {
177
+ for (let i = 0; i < job.results.length; i++) {
178
+ const page = job.results[i];
179
+ if (i > 0) outputContent("\n---\n\n");
180
+ outputContent(`# ${page.url}
181
+
182
+ `);
183
+ outputContent(page.markdown || page.html || "");
184
+ }
185
+ info(`
186
+ ${job.results.length} pages crawled`);
187
+ }
188
+ } catch (err) {
189
+ const msg = err instanceof Error ? err.message : String(err);
190
+ error(msg);
191
+ process.exit(1);
192
+ }
193
+ });
194
+ }
195
+
196
+ // src/commands/status.ts
197
+ import { ReaderClient as ReaderClient3 } from "@vakra-dev/reader-js";
198
+
199
+ // src/version.ts
200
+ var version = "0.1.0";
201
+
202
+ // src/commands/status.ts
203
+ function registerStatusCommand(program2) {
204
+ program2.command("status").description("Show CLI version, API connectivity, and credits").action(async () => {
205
+ const apiKey = getApiKey();
206
+ const apiUrl = getApiUrl();
207
+ const client = new ReaderClient3({ apiKey, baseUrl: apiUrl });
208
+ console.log(`Reader CLI v${version}`);
209
+ console.log(`API: ${apiUrl}`);
210
+ console.log(`Key: ${redactKey(apiKey)}`);
211
+ try {
212
+ const credits = await client.getCredits();
213
+ console.log(`Credits: ${credits.balance} / ${credits.limit} (${credits.tier} tier)`);
214
+ console.log(`Resets: ${credits.resetAt}`);
215
+ } catch (err) {
216
+ const msg = err instanceof Error ? err.message : String(err);
217
+ console.error(`API: connection failed (${msg})`);
218
+ process.exit(1);
219
+ }
220
+ });
221
+ }
222
+
223
+ // src/commands/credits.ts
224
+ import { ReaderClient as ReaderClient4 } from "@vakra-dev/reader-js";
225
+ function registerCreditsCommand(program2) {
226
+ program2.command("credits").description("Check credit balance and usage").option("--json", "Output full JSON response").action(async (opts) => {
227
+ const apiKey = getApiKey();
228
+ const client = new ReaderClient4({ apiKey, baseUrl: getApiUrl() });
229
+ try {
230
+ const credits = await client.getCredits();
231
+ if (opts.json) {
232
+ outputJson(credits);
233
+ return;
234
+ }
235
+ console.log(`Balance: ${credits.balance} / ${credits.limit}`);
236
+ console.log(`Used: ${credits.used}`);
237
+ console.log(`Tier: ${credits.tier}`);
238
+ console.log(`Resets: ${credits.resetAt}`);
239
+ } catch (err) {
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ error(msg);
242
+ process.exit(1);
243
+ }
244
+ });
245
+ }
246
+
247
+ // src/commands/config.ts
248
+ function registerConfigCommand(program2) {
249
+ const config = program2.command("config").description("Manage CLI configuration");
250
+ config.command("set <key> <value>").description("Set a config value (api-key, api-url)").action((key, value) => {
251
+ const current = loadConfig();
252
+ if (key === "api-key") {
253
+ current.apiKey = value;
254
+ saveConfig(current);
255
+ console.error(`API key saved: ${redactKey(value)}`);
256
+ } else if (key === "api-url") {
257
+ current.apiUrl = value;
258
+ saveConfig(current);
259
+ console.error(`API URL saved: ${value}`);
260
+ } else {
261
+ console.error(`Unknown config key: ${key}`);
262
+ console.error("Valid keys: api-key, api-url");
263
+ process.exit(1);
264
+ }
265
+ });
266
+ config.command("show").description("Show current configuration").action(() => {
267
+ const current = loadConfig();
268
+ const envKey = process.env.READER_API_KEY;
269
+ const envUrl = process.env.READER_API_URL;
270
+ console.log("Configuration:");
271
+ console.log("");
272
+ if (envKey) {
273
+ console.log(` API Key: ${redactKey(envKey)} (from READER_API_KEY env)`);
274
+ } else if (current.apiKey) {
275
+ console.log(` API Key: ${redactKey(current.apiKey)} (from ~/.reader/config.json)`);
276
+ } else {
277
+ console.log(" API Key: not configured");
278
+ }
279
+ if (envUrl) {
280
+ console.log(` API URL: ${envUrl} (from READER_API_URL env)`);
281
+ } else if (current.apiUrl) {
282
+ console.log(` API URL: ${current.apiUrl} (from ~/.reader/config.json)`);
283
+ } else {
284
+ console.log(" API URL: https://api.reader.dev (default)");
285
+ }
286
+ });
287
+ }
288
+
289
+ // src/index.ts
290
+ var program = new Command();
291
+ program.name("reader").description("Read the web for your AI agents. Powered by reader.dev.").version(version);
292
+ registerConfigCommand(program);
293
+ registerStatusCommand(program);
294
+ registerScrapeCommand(program);
295
+ registerCrawlCommand(program);
296
+ registerCreditsCommand(program);
297
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@vakra-dev/reader-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for the Reader API - read the web for your AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "reader": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsup --watch",
15
+ "pretest": "tsup",
16
+ "test": "vitest run"
17
+ },
18
+ "dependencies": {
19
+ "@vakra-dev/reader-js": "^0.2.0",
20
+ "commander": "^12.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "tsup": "^8.3.6",
24
+ "tsx": "^4.22.4",
25
+ "typescript": "^5.7.3",
26
+ "vitest": "^4.1.9"
27
+ },
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/vakra-dev/reader-cli.git"
32
+ },
33
+ "keywords": [
34
+ "reader",
35
+ "scraper",
36
+ "web",
37
+ "ai",
38
+ "cli",
39
+ "markdown",
40
+ "screenshot",
41
+ "crawl"
42
+ ]
43
+ }