@paperscope/cli
Advanced tools
+1
-1
| { | ||
| "name": "@paperscope/cli", | ||
| "version": "1.1.0", | ||
| "version": "1.2.0", | ||
| "description": "PaperScope CLI - Search and explore academic papers from your terminal", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+2
-0
@@ -56,2 +56,4 @@ # PaperScope CLI | ||
| > **Security note:** The API key is stored in plaintext at `~/.paperscope/config.json` with `0600` permissions (owner-only read/write). On shared or multi-user machines, prefer the `PAPERSCOPE_API_KEY` environment variable instead. | ||
| ## Usage | ||
@@ -58,0 +60,0 @@ |
+47
-16
@@ -19,2 +19,10 @@ import { getApiKey } from "./utils/config.js"; | ||
| const REQUEST_TIMEOUT_MS = 15_000; | ||
| const MAX_RETRIES = 1; | ||
| const RETRY_DELAY_MS = 1_000; | ||
| function sleep(ms: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
| async function request<T>(path: string, options: RequestOptions = {}): Promise<T> { | ||
@@ -40,23 +48,46 @@ const url = new URL(`${BASE_URL}${path}`); | ||
| const response = await fetch(url.toString(), { headers }); | ||
| let lastError: Error | undefined; | ||
| for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { | ||
| try { | ||
| const response = await fetch(url.toString(), { | ||
| headers, | ||
| signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), | ||
| }); | ||
| if (!response.ok) { | ||
| const body = await response.text(); | ||
| let message = `HTTP ${response.status}`; | ||
| try { | ||
| const json = JSON.parse(body); | ||
| if (typeof json.error === "object" && json.error?.message) { | ||
| message = json.error.message; | ||
| } else if (typeof json.error === "string") { | ||
| message = json.error; | ||
| } else if (json.message) { | ||
| message = json.message; | ||
| if (!response.ok) { | ||
| const body = await response.text(); | ||
| let message = `HTTP ${response.status}`; | ||
| try { | ||
| const json = JSON.parse(body); | ||
| if (typeof json.error === "object" && json.error?.message) { | ||
| message = json.error.message; | ||
| } else if (typeof json.error === "string") { | ||
| message = json.error; | ||
| } else if (json.message) { | ||
| message = json.message; | ||
| } | ||
| } catch { | ||
| if (body) message = body; | ||
| } | ||
| throw new ApiError(response.status, message); | ||
| } | ||
| } catch { | ||
| if (body) message = body; | ||
| return response.json() as Promise<T>; | ||
| } catch (err) { | ||
| if (err instanceof ApiError) { | ||
| // Do not retry client errors (4xx) | ||
| throw err; | ||
| } | ||
| lastError = err as Error; | ||
| if (attempt < MAX_RETRIES) { | ||
| await sleep(RETRY_DELAY_MS); | ||
| } | ||
| } | ||
| throw new ApiError(response.status, message); | ||
| } | ||
| return response.json() as Promise<T>; | ||
| // All retries exhausted | ||
| if (lastError?.name === "TimeoutError" || lastError?.name === "AbortError") { | ||
| throw new Error("Request timed out. Check your network."); | ||
| } | ||
| throw new Error(`Network error: ${lastError?.message || "unknown"}. Check your network.`); | ||
| } | ||
@@ -63,0 +94,0 @@ |
@@ -12,2 +12,6 @@ import chalk from "chalk"; | ||
| const limit = options.limit ? parseInt(options.limit, 10) : 10; | ||
| if (isNaN(limit) || limit <= 0 || limit > 100) { | ||
| console.error(chalk.red("\n Error: --limit must be a positive integer between 1 and 100.\n")); | ||
| process.exit(1); | ||
| } | ||
@@ -14,0 +18,0 @@ const spinner = ora({ |
| import chalk from "chalk"; | ||
| import { setApiKey, getApiKey } from "../utils/config.js"; | ||
| import { setApiKey, getApiKey, clearApiKey } from "../utils/config.js"; | ||
| import { createInterface } from "readline"; | ||
@@ -93,5 +93,4 @@ | ||
| } | ||
| const { clearApiKey } = require("../utils/config.js"); | ||
| clearApiKey(); | ||
| console.log(chalk.green("\n ✓ Logged out. API key removed.\n")); | ||
| } |
@@ -13,2 +13,8 @@ import chalk from "chalk"; | ||
| export async function readCommand(paperId: string, options: ReadOptions): Promise<void> { | ||
| const flagCount = [options.blog, options.summary, options.tags].filter(Boolean).length; | ||
| if (flagCount > 1) { | ||
| console.error(chalk.red("\n Error: --blog, --summary, and --tags are mutually exclusive. Please specify only one.\n")); | ||
| process.exit(1); | ||
| } | ||
| const spinner = ora({ | ||
@@ -15,0 +21,0 @@ text: `Fetching ${paperId}...`, |
@@ -13,3 +13,12 @@ import chalk from "chalk"; | ||
| export async function searchCommand(query: string, options: SearchOptions): Promise<void> { | ||
| if (!query.trim()) { | ||
| console.error(chalk.red("\n Error: Search query cannot be empty.\n")); | ||
| process.exit(1); | ||
| } | ||
| const limit = options.limit ? parseInt(options.limit, 10) : 10; | ||
| if (isNaN(limit) || limit <= 0 || limit > 100) { | ||
| console.error(chalk.red("\n Error: --limit must be a positive integer between 1 and 100.\n")); | ||
| process.exit(1); | ||
| } | ||
@@ -16,0 +25,0 @@ const spinner = ora({ |
@@ -13,2 +13,6 @@ import chalk from "chalk"; | ||
| const limit = options.limit ? parseInt(options.limit, 10) : 10; | ||
| if (isNaN(limit) || limit <= 0 || limit > 100) { | ||
| console.error(chalk.red("\n Error: --limit must be a positive integer between 1 and 100.\n")); | ||
| process.exit(1); | ||
| } | ||
@@ -15,0 +19,0 @@ const spinner = ora({ |
+3
-2
@@ -16,3 +16,3 @@ #!/usr/bin/env bun | ||
| .description("PaperScope CLI - Search and explore academic papers from your terminal") | ||
| .version("1.0.0"); | ||
| .version("1.2.0"); | ||
@@ -89,7 +89,8 @@ // ── search ─────────────────────────────────────────────── | ||
| // Default: show help | ||
| // Default: show help once and exit cleanly | ||
| if (process.argv.length <= 2) { | ||
| program.outputHelp(); | ||
| process.exit(0); | ||
| } | ||
| program.parse(process.argv); |
@@ -1,2 +0,2 @@ | ||
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; | ||
| import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs"; | ||
| import { homedir } from "os"; | ||
@@ -32,2 +32,3 @@ import { join } from "path"; | ||
| writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8"); | ||
| chmodSync(CONFIG_FILE, 0o600); | ||
| } | ||
@@ -34,0 +35,0 @@ |
42726
5.32%1078
4.86%208
0.97%