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.
- package/CHANGELOG.md +8 -0
- package/README.md +9 -0
- package/dist/cli.js +198 -1064
- package/package.json +4 -2
- package/src/cli/commands.ts +854 -0
- package/src/cli/config.ts +24 -0
- package/src/cli/display.ts +270 -0
- package/src/cli/errors.ts +31 -0
- package/src/cli/index.ts +239 -0
- package/src/cli/logger.ts +43 -0
- package/src/cli/types.ts +114 -0
- package/src/cli/utils.ts +239 -0
- package/src/cli/validators.ts +176 -0
- package/src/cli.ts +17 -0
- package/src/core/compat.ts +96 -0
- package/src/core/extractor.ts +532 -0
- package/src/core/fetcher.ts +592 -0
- package/src/core/formatter.ts +742 -0
- package/src/core/language-detector.ts +382 -0
- package/src/core/screenshot.ts +444 -0
- package/src/core/service-detector.ts +411 -0
- package/src/core/summarizer.ts +656 -0
- package/src/core/text-cleaner.ts +150 -0
- package/src/core/voice.ts +708 -0
|
@@ -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;
|
package/src/cli/index.ts
ADDED
|
@@ -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();
|