@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.
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/index.js +297 -0
- 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
|
+
}
|