glance-cli 0.12.0 → 0.13.1

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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Configuration module for Glance CLI
3
+ * Centralizes all configuration constants and settings
4
+ */
5
+
6
+ export const VERSION = "0.13.1";
7
+
8
+ export const CONFIG = {
9
+ VERSION,
10
+ MAX_CONTENT_SIZE: 10 * 1024 * 1024, // 10MB
11
+ FETCH_TIMEOUT: 30000, // 30s
12
+ RETRY_ATTEMPTS: 3,
13
+ RETRY_DELAY: 1000,
14
+ OLLAMA_ENDPOINT: process.env.OLLAMA_ENDPOINT || "http://localhost:11434",
15
+ } as const;
16
+
17
+ export const LANGUAGE_MAP: Record<string, string> = {
18
+ en: "English",
19
+ fr: "French",
20
+ es: "Spanish",
21
+ ht: "Haitian Creole",
22
+ } as const;
23
+
24
+ export const SUPPORTED_LANGUAGES = Object.keys(LANGUAGE_MAP);
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Display utilities for Glance CLI
3
+ * Handles help text, examples, and other user-facing output
4
+ */
5
+
6
+ import chalk from "chalk";
7
+ import { CONFIG } from "./config";
8
+ import type { ServiceStatus } from "./types";
9
+
10
+ export function showHelp(): void {
11
+ console.log(`
12
+ ${chalk.bold("glance")} v${CONFIG.VERSION} – AI-powered web reader
13
+
14
+ ${chalk.bold("Usage:")}
15
+ glance <url> [options]
16
+
17
+ ${chalk.bold("Options:")}
18
+ ${chalk.cyan("--help, -h")} Show this help message
19
+ ${chalk.cyan("--version, -v")} Show version number
20
+
21
+ ${chalk.bold("Summary Options:")}
22
+ ${chalk.cyan("--tldr")} Get a one-sentence summary
23
+ ${chalk.cyan("--key-points")} Extract key points as bullet points
24
+ ${chalk.cyan("--eli5")} Explain like I'm five
25
+ ${chalk.cyan("--full")} Read complete article without summarization ${chalk.green("(NEW!)")}
26
+ ${chalk.cyan("--ask, -q <question>")} Ask a specific question about the content
27
+
28
+ ${chalk.bold("Language & Voice:")}
29
+ ${chalk.cyan("--language, -l <lang>")} Output language (en, fr, es, ht)
30
+ ${chalk.cyan("--read, -r")} Read the summary aloud using text-to-speech
31
+ ${chalk.cyan("--voice <voice>")} Select voice for speech synthesis (e.g., nova, antoine)
32
+ ${chalk.cyan("--list-voices")} List all available voices by language
33
+ ${chalk.cyan("--audio-output <file>")} Save audio to file (mp3/wav)
34
+
35
+ ${chalk.bold("AI Provider Options:")}
36
+ ${chalk.cyan("--model, -m <model>")} Specify AI model (e.g., gpt-4o-mini, gemini-2.0-flash-exp, llama3)
37
+ ${chalk.cyan("--list-models")} List available Ollama models
38
+ ${chalk.cyan("--stream")} Stream the response as it's generated
39
+ ${chalk.cyan("--max-tokens <n>")} Maximum tokens for response (1-100000)
40
+ ${chalk.cyan("--free-only")} Only use free services (never use paid APIs)
41
+ ${chalk.cyan("--prefer-quality")} Prefer paid services for better quality
42
+
43
+ ${chalk.bold("Service Management:")}
44
+ ${chalk.cyan("--check-services")} Check available AI services and their status
45
+
46
+ ${chalk.bold("Output Format:")}
47
+ ${chalk.cyan("--format <type>")} Output format: md, json, plain (default: terminal)
48
+ ${chalk.cyan("--output, -o <file>")} Save to file (auto-detects format from extension)
49
+
50
+ ${chalk.bold("Advanced Options:")}
51
+ ${chalk.cyan("--full-render")} Enable JavaScript rendering (slower, for SPAs)
52
+ ${chalk.cyan("--screenshot <file>")} Capture a screenshot of the page
53
+ ${chalk.cyan("--metadata")} Show page metadata (author, dates, etc.)
54
+ ${chalk.cyan("--links")} Extract all links from the page
55
+ ${chalk.cyan("--debug")} Enable debug output
56
+
57
+ ${chalk.bold("Examples:")}
58
+ ${chalk.gray("# Quick summary")}
59
+ glance https://www.ayiti.ai
60
+
61
+ ${chalk.gray("# One-sentence summary with voice")}
62
+ glance https://news.site/article --tldr --read
63
+
64
+ ${chalk.gray("# Read full article with AI formatting")}
65
+ glance https://blog.com/post --full --read
66
+
67
+ ${chalk.gray("# Ask a specific question")}
68
+ glance https://docs.site/api --ask "How do I authenticate?"
69
+
70
+ ${chalk.gray("# French summary with French voice")}
71
+ glance https://www.ayiti.ai -l fr --voice antoine --read
72
+
73
+ ${chalk.gray("# Use specific AI model")}
74
+ glance https://www.ayiti.ai --model gpt-4o-mini
75
+
76
+ ${chalk.gray("# Check available services")}
77
+ glance --check-services
78
+
79
+ ${chalk.bold("Environment Variables:")}
80
+ ${chalk.cyan("OPENAI_API_KEY")} API key for OpenAI
81
+ ${chalk.cyan("GEMINI_API_KEY")} API key for Google Gemini
82
+ ${chalk.cyan("ELEVENLABS_API_KEY")} API key for ElevenLabs voice synthesis
83
+ ${chalk.cyan("OLLAMA_ENDPOINT")} Custom Ollama endpoint (default: http://localhost:11434)
84
+
85
+ ${chalk.dim("For more information: https://github.com/jkenley/glance-cli")}
86
+ `);
87
+ }
88
+
89
+ export function showVersion(): void {
90
+ console.log(`glance v${CONFIG.VERSION}`);
91
+ }
92
+
93
+ export function showExamples(): void {
94
+ console.log(`
95
+ ${chalk.bold("Glance CLI Examples")}
96
+
97
+ ${chalk.bold("Basic Usage:")}
98
+ ${chalk.gray("# Standard summary")}
99
+ glance https://www.ayiti.ai
100
+
101
+ ${chalk.gray("# One-sentence summary")}
102
+ glance https://www.ayiti.ai --tldr
103
+
104
+ ${chalk.gray("# Key points extraction")}
105
+ glance https://www.ayiti.ai --key-points
106
+
107
+ ${chalk.gray("# Simple explanation")}
108
+ glance https://www.ayiti.ai --eli5
109
+
110
+ ${chalk.gray("# Full article without summarization")}
111
+ glance https://www.ayiti.ai --full
112
+
113
+ ${chalk.bold("Voice & Audio:")}
114
+ ${chalk.gray("# Read summary aloud")}
115
+ glance https://www.ayiti.ai --tldr --read
116
+
117
+ ${chalk.gray("# Use specific voice")}
118
+ glance https://www.ayiti.ai --voice nova --read
119
+
120
+ ${chalk.gray("# Save as audio file")}
121
+ glance https://www.ayiti.ai --tldr --audio-output summary.mp3
122
+
123
+ ${chalk.gray("# List available voices")}
124
+ glance --list-voices
125
+
126
+ ${chalk.bold("Multilingual:")}
127
+ ${chalk.gray("# French summary with French voice")}
128
+ glance https://www.ayiti.ai -l fr --voice antoine --read
129
+
130
+ ${chalk.gray("# Spanish summary")}
131
+ glance https://www.ayiti.ai -l es
132
+
133
+ ${chalk.gray("# Translate French article to English")}
134
+ glance https://lemonde.fr/article --full -l en
135
+
136
+ ${chalk.bold("AI Models:")}
137
+ ${chalk.gray("# Use GPT-4")}
138
+ glance https://www.ayiti.ai --model gpt-4o-mini
139
+
140
+ ${chalk.gray("# Use Gemini")}
141
+ glance https://www.ayiti.ai --model gemini-2.0-flash-exp
142
+
143
+ ${chalk.gray("# Use local Ollama")}
144
+ glance https://www.ayiti.ai --model llama3
145
+
146
+ ${chalk.gray("# Force free services only")}
147
+ glance https://www.ayiti.ai --free-only
148
+
149
+ ${chalk.gray("# Prefer quality (paid) services")}
150
+ glance https://www.ayiti.ai --prefer-quality
151
+
152
+ ${chalk.bold("Advanced:")}
153
+ ${chalk.gray("# JavaScript-heavy sites")}
154
+ glance https://spa-site.com --full-render
155
+
156
+ ${chalk.gray("# Take screenshot")}
157
+ glance https://www.ayiti.ai --screenshot page.png
158
+
159
+ ${chalk.gray("# Extract metadata")}
160
+ glance https://www.ayiti.ai --metadata
161
+
162
+ ${chalk.gray("# Extract all links")}
163
+ glance https://www.ayiti.ai --links
164
+
165
+ ${chalk.gray("# Stream response")}
166
+ glance https://www.ayiti.ai --stream
167
+
168
+ ${chalk.gray("# Debug mode")}
169
+ glance https://www.ayiti.ai --debug
170
+ `);
171
+ }
172
+
173
+ export function formatErrorMessage(error: unknown): string {
174
+ if (
175
+ error &&
176
+ typeof error === "object" &&
177
+ "code" in error &&
178
+ error.code === "ENOTFOUND"
179
+ ) {
180
+ return chalk.red(
181
+ `Cannot reach the website. Please check your internet connection and the URL.`,
182
+ );
183
+ }
184
+
185
+ if (
186
+ error &&
187
+ typeof error === "object" &&
188
+ "code" in error &&
189
+ error.code === "ETIMEDOUT"
190
+ ) {
191
+ return chalk.red(
192
+ `Request timed out. The website might be slow or unresponsive.`,
193
+ );
194
+ }
195
+
196
+ if (error && typeof error === "object" && "userMessage" in error) {
197
+ return chalk.red(String(error.userMessage));
198
+ }
199
+
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ return chalk.red(`Error: ${message || "Unknown error occurred"}`);
202
+ }
203
+
204
+ export function showServiceStatus(services: ServiceStatus): void {
205
+ if (!services) {
206
+ console.error(
207
+ chalk.red("Service detection failed - no services information available"),
208
+ );
209
+ return;
210
+ }
211
+
212
+ console.log(chalk.bold("\nšŸ” Service Detection Results:\n"));
213
+
214
+ const formatStatus = (available: boolean) =>
215
+ available ? chalk.green("āœ… Available") : chalk.gray("āŒ Not Available");
216
+
217
+ console.log(chalk.bold("AI Services:"));
218
+
219
+ if (services.ollama) {
220
+ console.log(
221
+ ` Ollama (Local): ${formatStatus(services.ollama.available)} ${services.ollama.available && services.ollama.models ? chalk.gray(`(${services.ollama.models.length} models)`) : ""}`,
222
+ );
223
+ }
224
+
225
+ if (services.openai) {
226
+ console.log(
227
+ ` OpenAI: ${formatStatus(services.openai.available)}`,
228
+ );
229
+ }
230
+
231
+ if (services.gemini) {
232
+ console.log(
233
+ ` Google Gemini: ${formatStatus(services.gemini.available)}`,
234
+ );
235
+ }
236
+
237
+ console.log(chalk.bold("\nVoice Services:"));
238
+
239
+ if (services.elevenlabs) {
240
+ console.log(
241
+ ` ElevenLabs: ${formatStatus(services.elevenlabs.available)} ${services.elevenlabs.available && services.elevenlabs.voices ? chalk.gray(`(${services.elevenlabs.voices.length} voices)`) : ""}`,
242
+ );
243
+ }
244
+
245
+ console.log(
246
+ ` System TTS: ${formatStatus(true)} ${chalk.gray("(fallback)")}`,
247
+ );
248
+
249
+ console.log(chalk.bold("\nDefault Configuration:"));
250
+ console.log(
251
+ ` Default AI Model: ${chalk.cyan(services.defaultModel || "None available")}`,
252
+ );
253
+ console.log(
254
+ ` Priority: ${chalk.cyan(services.priority || "Free services first")}`,
255
+ );
256
+
257
+ if (services.recommendations && services.recommendations.length > 0) {
258
+ console.log(chalk.bold("\nšŸ’” Recommendations:"));
259
+ services.recommendations.forEach((rec: string) => {
260
+ console.log(` • ${rec}`);
261
+ });
262
+ }
263
+
264
+ console.log(
265
+ chalk.dim(
266
+ "\nFor setup instructions, visit: https://github.com/jkenley/glance-cli#setup",
267
+ ),
268
+ );
269
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Custom error types for Glance CLI
3
+ */
4
+
5
+ export class GlanceError extends Error {
6
+ constructor(
7
+ message: string,
8
+ public code: string,
9
+ public userMessage: string,
10
+ public recoverable: boolean = false,
11
+ public hint?: string,
12
+ ) {
13
+ super(message);
14
+ this.name = "GlanceError";
15
+ }
16
+ }
17
+
18
+ export const ErrorCodes = {
19
+ INVALID_URL: "INVALID_URL",
20
+ INVALID_LANGUAGE: "INVALID_LANGUAGE",
21
+ INVALID_MAX_TOKENS: "INVALID_MAX_TOKENS",
22
+ API_KEY_MISSING: "API_KEY_MISSING",
23
+ API_KEY_INVALID: "API_KEY_INVALID",
24
+ FETCH_FAILED: "FETCH_FAILED",
25
+ CONTENT_TOO_LARGE: "CONTENT_TOO_LARGE",
26
+ SUMMARIZE_FAILED: "SUMMARIZE_FAILED",
27
+ CACHE_ERROR: "CACHE_ERROR",
28
+ VOICE_SYNTHESIS_FAILED: "VOICE_SYNTHESIS_FAILED",
29
+ SCREENSHOT_FAILED: "SCREENSHOT_FAILED",
30
+ EXPORT_FAILED: "EXPORT_FAILED",
31
+ } as const;
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Main CLI entry point
3
+ * This is a cleaner, modular version that exports all components
4
+ */
5
+
6
+ import { parseArgs } from "node:util";
7
+ import chalk from "chalk";
8
+ import {
9
+ checkServicesCommand,
10
+ type GlanceOptions,
11
+ glance,
12
+ listModelsCommand,
13
+ listVoicesCommand,
14
+ } from "./commands";
15
+ import { formatErrorMessage, showHelp, showVersion } from "./display";
16
+ // Import modules
17
+ import { GlanceError } from "./errors";
18
+ import { logger } from "./logger";
19
+ import { validateLanguage, validateMaxTokens, validateURL } from "./validators";
20
+
21
+ export * from "./commands";
22
+ // Export all modules for programmatic use
23
+ export * from "./config";
24
+ export * from "./display";
25
+ export * from "./errors";
26
+ export * from "./logger";
27
+ export * from "./types";
28
+ export * from "./utils";
29
+ export * from "./validators";
30
+
31
+ /**
32
+ * Parse CLI arguments
33
+ */
34
+ function parseCliArgs() {
35
+ try {
36
+ const { values, positionals } = parseArgs({
37
+ args: process.argv.slice(2),
38
+ allowPositionals: true,
39
+ options: {
40
+ // Core options
41
+ help: { type: "boolean", short: "h" },
42
+ version: { type: "boolean", short: "v" },
43
+
44
+ // Summary options
45
+ tldr: { type: "boolean" },
46
+ "key-points": { type: "boolean" },
47
+ eli5: { type: "boolean" },
48
+ full: { type: "boolean" },
49
+ ask: { type: "string", short: "q" },
50
+
51
+ // Language options
52
+ language: { type: "string", short: "l" },
53
+
54
+ // Voice options
55
+ read: { type: "boolean", short: "r" },
56
+ voice: { type: "string" },
57
+ "list-voices": { type: "boolean" },
58
+ "audio-output": { type: "string" },
59
+
60
+ // AI options
61
+ model: { type: "string", short: "m" },
62
+ "list-models": { type: "boolean" },
63
+ stream: { type: "boolean" },
64
+ "max-tokens": { type: "string" },
65
+
66
+ // Service options
67
+ "check-services": { type: "boolean" },
68
+ "free-only": { type: "boolean" },
69
+ "prefer-quality": { type: "boolean" },
70
+
71
+ // Format & Output options
72
+ format: { type: "string" },
73
+ output: { type: "string", short: "o" },
74
+
75
+ // Advanced options
76
+ "full-render": { type: "boolean" },
77
+ screenshot: { type: "string" },
78
+ metadata: { type: "boolean" },
79
+ links: { type: "boolean" },
80
+ debug: { type: "boolean" },
81
+ },
82
+ });
83
+
84
+ return { values, positionals };
85
+ } catch (error: unknown) {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ console.error(chalk.red(`Error parsing arguments: ${message}`));
88
+ console.log(chalk.dim("Run 'glance --help' for usage information"));
89
+ process.exit(1);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Main CLI function
95
+ */
96
+ export async function runCli() {
97
+ try {
98
+ const { values, positionals } = parseCliArgs();
99
+
100
+ // Handle help
101
+ if (values.help) {
102
+ showHelp();
103
+ process.exit(0);
104
+ }
105
+
106
+ // Handle version
107
+ if (values.version) {
108
+ showVersion();
109
+ process.exit(0);
110
+ }
111
+
112
+ // Handle special commands that don't require a URL
113
+
114
+ if (values["list-voices"]) {
115
+ await listVoicesCommand();
116
+ process.exit(0);
117
+ }
118
+
119
+ if (values["check-services"]) {
120
+ await checkServicesCommand();
121
+ process.exit(0);
122
+ }
123
+
124
+ if (values["list-models"]) {
125
+ await listModelsCommand();
126
+ process.exit(0);
127
+ }
128
+
129
+ // Validate URL is provided
130
+ if (positionals.length === 0) {
131
+ console.error(chalk.red("Error: No URL provided"));
132
+ console.log(chalk.dim("Usage: glance <url> [options]"));
133
+ console.log(chalk.dim("Run 'glance --help' for more information"));
134
+ process.exit(1);
135
+ }
136
+
137
+ const url = positionals[0];
138
+
139
+ if (!url) {
140
+ console.error(chalk.red("Error: URL is required."));
141
+ process.exit(1);
142
+ }
143
+
144
+ // Validate URL format
145
+ const urlValidation = validateURL(url);
146
+
147
+ if (!urlValidation.valid) {
148
+ console.error(chalk.red(`Error: ${urlValidation.error}`));
149
+ process.exit(1);
150
+ }
151
+
152
+ // Validate language if provided
153
+ if (values.language) {
154
+ const langValidation = validateLanguage(values.language);
155
+ if (!langValidation.valid) {
156
+ console.error(chalk.red(`Error: ${langValidation.error}`));
157
+ process.exit(1);
158
+ }
159
+ }
160
+
161
+ // Validate max tokens if provided
162
+ let maxTokens: number | undefined;
163
+ if (values["max-tokens"]) {
164
+ const tokensValidation = validateMaxTokens(values["max-tokens"]);
165
+ if (!tokensValidation.valid) {
166
+ console.error(chalk.red(`Error: ${tokensValidation.error}`));
167
+ process.exit(1);
168
+ }
169
+ maxTokens = tokensValidation.parsed;
170
+ }
171
+
172
+ // Prepare options
173
+ const options: GlanceOptions = {
174
+ model: values.model,
175
+ language: values.language,
176
+ tldr: values.tldr,
177
+ keyPoints: values["key-points"],
178
+ eli5: values.eli5,
179
+ full: values.full,
180
+ customQuestion: values.ask,
181
+ stream: values.stream,
182
+ maxTokens,
183
+ format: values.format,
184
+ output: values.output,
185
+ screenshot: values.screenshot,
186
+ fullRender: values["full-render"],
187
+ metadata: values.metadata,
188
+ links: values.links,
189
+ read: values.read,
190
+ voice: values.voice,
191
+ audioOutput: values["audio-output"],
192
+ freeOnly: values["free-only"],
193
+ preferQuality: values["prefer-quality"],
194
+ debug: values.debug,
195
+ };
196
+
197
+ // Run the main command
198
+ const result = await glance(url, options);
199
+
200
+ // Output result (if not already handled by streaming or voice)
201
+ if (!options.stream && !options.read && !options.audioOutput) {
202
+ console.log(result);
203
+ }
204
+ } catch (error: unknown) {
205
+ // Handle errors gracefully
206
+ if (error instanceof GlanceError) {
207
+ console.error(formatErrorMessage(error));
208
+ if (error.hint) {
209
+ console.log(chalk.yellow(`\nšŸ’” Hint: ${error.hint}`));
210
+ }
211
+ } else {
212
+ const errorMessage =
213
+ error instanceof Error ? error.message : String(error);
214
+ console.error(chalk.red(`\nāŒ Unexpected error: ${errorMessage}`));
215
+ if (logger.getLevel() === "debug" && error instanceof Error) {
216
+ console.error(error.stack);
217
+ } else {
218
+ console.log(chalk.dim("Run with --debug for more details"));
219
+ }
220
+ }
221
+
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ // Run CLI if this is the main module
227
+ // Use process check for Bun compatibility
228
+ if (
229
+ typeof process !== "undefined" &&
230
+ process.argv &&
231
+ process.argv[1] &&
232
+ process.argv[1].endsWith("index.ts")
233
+ ) {
234
+ runCli();
235
+ } else if (typeof require !== "undefined" && require.main === module) {
236
+ runCli();
237
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Logging utility for Glance CLI
3
+ */
4
+
5
+ import chalk from "chalk";
6
+
7
+ export type LogLevel = "debug" | "info" | "warn" | "error";
8
+
9
+ class Logger {
10
+ private level: LogLevel = "info";
11
+
12
+ setLevel(level: LogLevel) {
13
+ this.level = level;
14
+ }
15
+
16
+ getLevel(): LogLevel {
17
+ return this.level;
18
+ }
19
+
20
+ debug(...args: unknown[]) {
21
+ if (this.level === "debug") {
22
+ console.log(chalk.gray("[DEBUG]"), ...args);
23
+ }
24
+ }
25
+
26
+ info(...args: unknown[]) {
27
+ if (["debug", "info"].includes(this.level)) {
28
+ console.log(chalk.blue("[INFO]"), ...args);
29
+ }
30
+ }
31
+
32
+ warn(...args: unknown[]) {
33
+ if (["debug", "info", "warn"].includes(this.level)) {
34
+ console.warn(chalk.yellow("[WARN]"), ...args);
35
+ }
36
+ }
37
+
38
+ error(...args: unknown[]) {
39
+ console.error(chalk.red("[ERROR]"), ...args);
40
+ }
41
+ }
42
+
43
+ export const logger = new Logger();