glance-cli 0.13.0 → 0.14.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.
@@ -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.14.0";
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,270 @@
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
+ ${chalk.cyan("--copy, -c")} Copy summary to clipboard
50
+
51
+ ${chalk.bold("Advanced Options:")}
52
+ ${chalk.cyan("--full-render")} Enable JavaScript rendering (slower, for SPAs)
53
+ ${chalk.cyan("--screenshot <file>")} Capture a screenshot of the page
54
+ ${chalk.cyan("--metadata")} Show page metadata (author, dates, etc.)
55
+ ${chalk.cyan("--links")} Extract all links from the page
56
+ ${chalk.cyan("--debug")} Enable debug output
57
+
58
+ ${chalk.bold("Examples:")}
59
+ ${chalk.gray("# Quick summary")}
60
+ glance https://www.ayiti.ai
61
+
62
+ ${chalk.gray("# One-sentence summary with voice")}
63
+ glance https://news.site/article --tldr --read
64
+
65
+ ${chalk.gray("# Read full article with AI formatting")}
66
+ glance https://blog.com/post --full --read
67
+
68
+ ${chalk.gray("# Ask a specific question")}
69
+ glance https://docs.site/api --ask "How do I authenticate?"
70
+
71
+ ${chalk.gray("# French summary with French voice")}
72
+ glance https://www.ayiti.ai -l fr --voice antoine --read
73
+
74
+ ${chalk.gray("# Use specific AI model")}
75
+ glance https://www.ayiti.ai --model gpt-4o-mini
76
+
77
+ ${chalk.gray("# Check available services")}
78
+ glance --check-services
79
+
80
+ ${chalk.bold("Environment Variables:")}
81
+ ${chalk.cyan("OPENAI_API_KEY")} API key for OpenAI
82
+ ${chalk.cyan("GEMINI_API_KEY")} API key for Google Gemini
83
+ ${chalk.cyan("ELEVENLABS_API_KEY")} API key for ElevenLabs voice synthesis
84
+ ${chalk.cyan("OLLAMA_ENDPOINT")} Custom Ollama endpoint (default: http://localhost:11434)
85
+
86
+ ${chalk.dim("For more information: https://github.com/jkenley/glance-cli")}
87
+ `);
88
+ }
89
+
90
+ export function showVersion(): void {
91
+ console.log(`glance v${CONFIG.VERSION}`);
92
+ }
93
+
94
+ export function showExamples(): void {
95
+ console.log(`
96
+ ${chalk.bold("Glance CLI Examples")}
97
+
98
+ ${chalk.bold("Basic Usage:")}
99
+ ${chalk.gray("# Standard summary")}
100
+ glance https://www.ayiti.ai
101
+
102
+ ${chalk.gray("# One-sentence summary")}
103
+ glance https://www.ayiti.ai --tldr
104
+
105
+ ${chalk.gray("# Key points extraction")}
106
+ glance https://www.ayiti.ai --key-points
107
+
108
+ ${chalk.gray("# Simple explanation")}
109
+ glance https://www.ayiti.ai --eli5
110
+
111
+ ${chalk.gray("# Full article without summarization")}
112
+ glance https://www.ayiti.ai --full
113
+
114
+ ${chalk.bold("Voice & Audio:")}
115
+ ${chalk.gray("# Read summary aloud")}
116
+ glance https://www.ayiti.ai --tldr --read
117
+
118
+ ${chalk.gray("# Use specific voice")}
119
+ glance https://www.ayiti.ai --voice nova --read
120
+
121
+ ${chalk.gray("# Save as audio file")}
122
+ glance https://www.ayiti.ai --tldr --audio-output summary.mp3
123
+
124
+ ${chalk.gray("# List available voices")}
125
+ glance --list-voices
126
+
127
+ ${chalk.bold("Multilingual:")}
128
+ ${chalk.gray("# French summary with French voice")}
129
+ glance https://www.ayiti.ai -l fr --voice antoine --read
130
+
131
+ ${chalk.gray("# Spanish summary")}
132
+ glance https://www.ayiti.ai -l es
133
+
134
+ ${chalk.gray("# Translate French article to English")}
135
+ glance https://lemonde.fr/article --full -l en
136
+
137
+ ${chalk.bold("AI Models:")}
138
+ ${chalk.gray("# Use GPT-4")}
139
+ glance https://www.ayiti.ai --model gpt-4o-mini
140
+
141
+ ${chalk.gray("# Use Gemini")}
142
+ glance https://www.ayiti.ai --model gemini-2.0-flash-exp
143
+
144
+ ${chalk.gray("# Use local Ollama")}
145
+ glance https://www.ayiti.ai --model llama3
146
+
147
+ ${chalk.gray("# Force free services only")}
148
+ glance https://www.ayiti.ai --free-only
149
+
150
+ ${chalk.gray("# Prefer quality (paid) services")}
151
+ glance https://www.ayiti.ai --prefer-quality
152
+
153
+ ${chalk.bold("Advanced:")}
154
+ ${chalk.gray("# JavaScript-heavy sites")}
155
+ glance https://spa-site.com --full-render
156
+
157
+ ${chalk.gray("# Take screenshot")}
158
+ glance https://www.ayiti.ai --screenshot page.png
159
+
160
+ ${chalk.gray("# Extract metadata")}
161
+ glance https://www.ayiti.ai --metadata
162
+
163
+ ${chalk.gray("# Extract all links")}
164
+ glance https://www.ayiti.ai --links
165
+
166
+ ${chalk.gray("# Stream response")}
167
+ glance https://www.ayiti.ai --stream
168
+
169
+ ${chalk.gray("# Debug mode")}
170
+ glance https://www.ayiti.ai --debug
171
+ `);
172
+ }
173
+
174
+ export function formatErrorMessage(error: unknown): string {
175
+ if (
176
+ error &&
177
+ typeof error === "object" &&
178
+ "code" in error &&
179
+ error.code === "ENOTFOUND"
180
+ ) {
181
+ return chalk.red(
182
+ `Cannot reach the website. Please check your internet connection and the URL.`,
183
+ );
184
+ }
185
+
186
+ if (
187
+ error &&
188
+ typeof error === "object" &&
189
+ "code" in error &&
190
+ error.code === "ETIMEDOUT"
191
+ ) {
192
+ return chalk.red(
193
+ `Request timed out. The website might be slow or unresponsive.`,
194
+ );
195
+ }
196
+
197
+ if (error && typeof error === "object" && "userMessage" in error) {
198
+ return chalk.red(String(error.userMessage));
199
+ }
200
+
201
+ const message = error instanceof Error ? error.message : String(error);
202
+ return chalk.red(`Error: ${message || "Unknown error occurred"}`);
203
+ }
204
+
205
+ export function showServiceStatus(services: ServiceStatus): void {
206
+ if (!services) {
207
+ console.error(
208
+ chalk.red("Service detection failed - no services information available"),
209
+ );
210
+ return;
211
+ }
212
+
213
+ console.log(chalk.bold("\nšŸ” Service Detection Results:\n"));
214
+
215
+ const formatStatus = (available: boolean) =>
216
+ available ? chalk.green("āœ… Available") : chalk.gray("āŒ Not Available");
217
+
218
+ console.log(chalk.bold("AI Services:"));
219
+
220
+ if (services.ollama) {
221
+ console.log(
222
+ ` Ollama (Local): ${formatStatus(services.ollama.available)} ${services.ollama.available && services.ollama.models ? chalk.gray(`(${services.ollama.models.length} models)`) : ""}`,
223
+ );
224
+ }
225
+
226
+ if (services.openai) {
227
+ console.log(
228
+ ` OpenAI: ${formatStatus(services.openai.available)}`,
229
+ );
230
+ }
231
+
232
+ if (services.gemini) {
233
+ console.log(
234
+ ` Google Gemini: ${formatStatus(services.gemini.available)}`,
235
+ );
236
+ }
237
+
238
+ console.log(chalk.bold("\nVoice Services:"));
239
+
240
+ if (services.elevenlabs) {
241
+ console.log(
242
+ ` ElevenLabs: ${formatStatus(services.elevenlabs.available)} ${services.elevenlabs.available && services.elevenlabs.voices ? chalk.gray(`(${services.elevenlabs.voices.length} voices)`) : ""}`,
243
+ );
244
+ }
245
+
246
+ console.log(
247
+ ` System TTS: ${formatStatus(true)} ${chalk.gray("(fallback)")}`,
248
+ );
249
+
250
+ console.log(chalk.bold("\nDefault Configuration:"));
251
+ console.log(
252
+ ` Default AI Model: ${chalk.cyan(services.defaultModel || "None available")}`,
253
+ );
254
+ console.log(
255
+ ` Priority: ${chalk.cyan(services.priority || "Free services first")}`,
256
+ );
257
+
258
+ if (services.recommendations && services.recommendations.length > 0) {
259
+ console.log(chalk.bold("\nšŸ’” Recommendations:"));
260
+ services.recommendations.forEach((rec: string) => {
261
+ console.log(` • ${rec}`);
262
+ });
263
+ }
264
+
265
+ console.log(
266
+ chalk.dim(
267
+ "\nFor setup instructions, visit: https://github.com/jkenley/glance-cli#setup",
268
+ ),
269
+ );
270
+ }
@@ -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,239 @@
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
+ copy: { type: "boolean", short: "c" },
75
+
76
+ // Advanced options
77
+ "full-render": { type: "boolean" },
78
+ screenshot: { type: "string" },
79
+ metadata: { type: "boolean" },
80
+ links: { type: "boolean" },
81
+ debug: { type: "boolean" },
82
+ },
83
+ });
84
+
85
+ return { values, positionals };
86
+ } catch (error: unknown) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ console.error(chalk.red(`Error parsing arguments: ${message}`));
89
+ console.log(chalk.dim("Run 'glance --help' for usage information"));
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Main CLI function
96
+ */
97
+ export async function runCli() {
98
+ try {
99
+ const { values, positionals } = parseCliArgs();
100
+
101
+ // Handle help
102
+ if (values.help) {
103
+ showHelp();
104
+ process.exit(0);
105
+ }
106
+
107
+ // Handle version
108
+ if (values.version) {
109
+ showVersion();
110
+ process.exit(0);
111
+ }
112
+
113
+ // Handle special commands that don't require a URL
114
+
115
+ if (values["list-voices"]) {
116
+ await listVoicesCommand();
117
+ process.exit(0);
118
+ }
119
+
120
+ if (values["check-services"]) {
121
+ await checkServicesCommand();
122
+ process.exit(0);
123
+ }
124
+
125
+ if (values["list-models"]) {
126
+ await listModelsCommand();
127
+ process.exit(0);
128
+ }
129
+
130
+ // Validate URL is provided
131
+ if (positionals.length === 0) {
132
+ console.error(chalk.red("Error: No URL provided"));
133
+ console.log(chalk.dim("Usage: glance <url> [options]"));
134
+ console.log(chalk.dim("Run 'glance --help' for more information"));
135
+ process.exit(1);
136
+ }
137
+
138
+ const url = positionals[0];
139
+
140
+ if (!url) {
141
+ console.error(chalk.red("Error: URL is required."));
142
+ process.exit(1);
143
+ }
144
+
145
+ // Validate URL format
146
+ const urlValidation = validateURL(url);
147
+
148
+ if (!urlValidation.valid) {
149
+ console.error(chalk.red(`Error: ${urlValidation.error}`));
150
+ process.exit(1);
151
+ }
152
+
153
+ // Validate language if provided
154
+ if (values.language) {
155
+ const langValidation = validateLanguage(values.language);
156
+ if (!langValidation.valid) {
157
+ console.error(chalk.red(`Error: ${langValidation.error}`));
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ // Validate max tokens if provided
163
+ let maxTokens: number | undefined;
164
+ if (values["max-tokens"]) {
165
+ const tokensValidation = validateMaxTokens(values["max-tokens"]);
166
+ if (!tokensValidation.valid) {
167
+ console.error(chalk.red(`Error: ${tokensValidation.error}`));
168
+ process.exit(1);
169
+ }
170
+ maxTokens = tokensValidation.parsed;
171
+ }
172
+
173
+ // Prepare options
174
+ const options: GlanceOptions = {
175
+ model: values.model,
176
+ language: values.language,
177
+ tldr: values.tldr,
178
+ keyPoints: values["key-points"],
179
+ eli5: values.eli5,
180
+ full: values.full,
181
+ customQuestion: values.ask,
182
+ stream: values.stream,
183
+ maxTokens,
184
+ format: values.format,
185
+ output: values.output,
186
+ screenshot: values.screenshot,
187
+ fullRender: values["full-render"],
188
+ metadata: values.metadata,
189
+ links: values.links,
190
+ read: values.read,
191
+ voice: values.voice,
192
+ audioOutput: values["audio-output"],
193
+ freeOnly: values["free-only"],
194
+ preferQuality: values["prefer-quality"],
195
+ debug: values.debug,
196
+ copy: values.copy,
197
+ };
198
+
199
+ // Run the main command
200
+ const result = await glance(url, options);
201
+
202
+ // Output result (if not already handled by streaming or voice)
203
+ if (!options.stream && !options.read && !options.audioOutput) {
204
+ console.log(result);
205
+ }
206
+ } catch (error: unknown) {
207
+ // Handle errors gracefully
208
+ if (error instanceof GlanceError) {
209
+ console.error(formatErrorMessage(error));
210
+ if (error.hint) {
211
+ console.log(chalk.yellow(`\nšŸ’” Hint: ${error.hint}`));
212
+ }
213
+ } else {
214
+ const errorMessage =
215
+ error instanceof Error ? error.message : String(error);
216
+ console.error(chalk.red(`\nāŒ Unexpected error: ${errorMessage}`));
217
+ if (logger.getLevel() === "debug" && error instanceof Error) {
218
+ console.error(error.stack);
219
+ } else {
220
+ console.log(chalk.dim("Run with --debug for more details"));
221
+ }
222
+ }
223
+
224
+ process.exit(1);
225
+ }
226
+ }
227
+
228
+ // Run CLI if this is the main module
229
+ // Use process check for Bun compatibility
230
+ if (
231
+ typeof process !== "undefined" &&
232
+ process.argv &&
233
+ process.argv[1] &&
234
+ process.argv[1].endsWith("index.ts")
235
+ ) {
236
+ runCli();
237
+ } else if (typeof require !== "undefined" && require.main === module) {
238
+ runCli();
239
+ }
@@ -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();