webpeel 0.2.0 → 0.3.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,392 @@
1
+ /**
2
+ * CLI Authentication & Usage Tracking
3
+ *
4
+ * Handles:
5
+ * - Anonymous usage (25 free fetches)
6
+ * - API key authentication
7
+ * - Usage checking against API
8
+ * - Config file management (~/.webpeel/config.json)
9
+ */
10
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs';
11
+ import { homedir } from 'os';
12
+ import { join } from 'path';
13
+ import * as readline from 'readline';
14
+ // Config file location: ~/.webpeel/config.json
15
+ const CONFIG_DIR = join(homedir(), '.webpeel');
16
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
17
+ // API base URL (configurable via env var)
18
+ const API_BASE_URL = process.env.WEBPEEL_API_URL || 'https://webpeel-api.onrender.com';
19
+ /**
20
+ * Load config from ~/.webpeel/config.json
21
+ */
22
+ export function loadConfig() {
23
+ try {
24
+ if (!existsSync(CONFIG_FILE)) {
25
+ return {
26
+ anonymousUsage: 0,
27
+ lastReset: getLastMonday().toISOString(),
28
+ };
29
+ }
30
+ const content = readFileSync(CONFIG_FILE, 'utf-8');
31
+ const config = JSON.parse(content);
32
+ // Ensure lastReset exists
33
+ if (!config.lastReset) {
34
+ config.lastReset = getLastMonday().toISOString();
35
+ }
36
+ return config;
37
+ }
38
+ catch (error) {
39
+ // If config is corrupted, start fresh
40
+ return {
41
+ anonymousUsage: 0,
42
+ lastReset: getLastMonday().toISOString(),
43
+ };
44
+ }
45
+ }
46
+ /**
47
+ * Save config to ~/.webpeel/config.json
48
+ */
49
+ export function saveConfig(config) {
50
+ try {
51
+ // Ensure directory exists
52
+ if (!existsSync(CONFIG_DIR)) {
53
+ mkdirSync(CONFIG_DIR, { recursive: true });
54
+ }
55
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
56
+ }
57
+ catch (error) {
58
+ console.error(`Warning: Failed to save config: ${error instanceof Error ? error.message : 'Unknown error'}`);
59
+ }
60
+ }
61
+ /**
62
+ * Delete config file
63
+ */
64
+ export function deleteConfig() {
65
+ try {
66
+ if (existsSync(CONFIG_FILE)) {
67
+ unlinkSync(CONFIG_FILE);
68
+ }
69
+ }
70
+ catch (error) {
71
+ console.error(`Warning: Failed to delete config: ${error instanceof Error ? error.message : 'Unknown error'}`);
72
+ }
73
+ }
74
+ /**
75
+ * Get the last Monday 00:00 UTC (start of current week)
76
+ */
77
+ function getLastMonday() {
78
+ const now = new Date();
79
+ const day = now.getUTCDay();
80
+ const diff = day === 0 ? 6 : day - 1; // Days since last Monday
81
+ const lastMonday = new Date(now);
82
+ lastMonday.setUTCDate(now.getUTCDate() - diff);
83
+ lastMonday.setUTCHours(0, 0, 0, 0);
84
+ return lastMonday;
85
+ }
86
+ /**
87
+ * Check if anonymous usage counter needs to be reset (weekly reset)
88
+ */
89
+ function shouldResetAnonymousUsage(config) {
90
+ const lastReset = new Date(config.lastReset);
91
+ const currentWeekStart = getLastMonday();
92
+ return lastReset < currentWeekStart;
93
+ }
94
+ /**
95
+ * Check usage quota before making a request
96
+ */
97
+ export async function checkUsage() {
98
+ const config = loadConfig();
99
+ // Check if anonymous usage needs reset
100
+ if (shouldResetAnonymousUsage(config)) {
101
+ config.anonymousUsage = 0;
102
+ config.lastReset = getLastMonday().toISOString();
103
+ saveConfig(config);
104
+ }
105
+ // Anonymous user - allow first 25 fetches
106
+ if (!config.apiKey) {
107
+ const limit = 25;
108
+ const used = config.anonymousUsage;
109
+ const remaining = limit - used;
110
+ if (used >= limit) {
111
+ return {
112
+ allowed: false,
113
+ message: `You've used your ${limit} free fetches.\n\nSign up for free at https://app.webpeel.dev/signup to get 125 fetches/week.\nOr run: webpeel login\n`,
114
+ };
115
+ }
116
+ // Increment usage counter
117
+ config.anonymousUsage++;
118
+ saveConfig(config);
119
+ return {
120
+ allowed: true,
121
+ isAnonymous: true,
122
+ usageInfo: {
123
+ used: used + 1,
124
+ limit,
125
+ remaining: remaining - 1,
126
+ },
127
+ };
128
+ }
129
+ // Authenticated user - check with API
130
+ try {
131
+ const response = await fetch(`${API_BASE_URL}/v1/usage`, {
132
+ headers: {
133
+ 'Authorization': `Bearer ${config.apiKey}`,
134
+ },
135
+ });
136
+ if (!response.ok) {
137
+ if (response.status === 401) {
138
+ return {
139
+ allowed: false,
140
+ message: `Authentication failed. Your API key may be invalid.\n\nRun: webpeel logout\nThen: webpeel login\n`,
141
+ };
142
+ }
143
+ // If API returns other errors, allow gracefully
144
+ return { allowed: true };
145
+ }
146
+ const data = await response.json();
147
+ // Check burst limit
148
+ if (data.session.burstUsed >= data.session.burstLimit) {
149
+ const resetsIn = data.session.resetsIn || 'soon';
150
+ return {
151
+ allowed: false,
152
+ message: `Burst limit reached (${data.session.burstUsed}/${data.session.burstLimit}). Resets in ${resetsIn}.\nUpgrade: https://webpeel.dev/#pricing`,
153
+ };
154
+ }
155
+ // Check weekly limit
156
+ if (data.weekly.remaining <= 0 && !data.extraUsage?.enabled) {
157
+ return {
158
+ allowed: false,
159
+ message: `Weekly limit reached (${data.weekly.totalUsed}/${data.weekly.totalAvailable}).\nResets: ${data.weekly.resetsAt}\nEnable extra usage or upgrade: https://app.webpeel.dev/billing`,
160
+ };
161
+ }
162
+ // Cache plan tier for offline feature gating
163
+ const planName = (data.plan?.name || 'free').toLowerCase();
164
+ config.planTier = planName;
165
+ config.planCachedAt = new Date().toISOString();
166
+ saveConfig(config);
167
+ return {
168
+ allowed: true,
169
+ isAnonymous: false,
170
+ usageInfo: {
171
+ used: data.weekly.totalUsed,
172
+ limit: data.weekly.totalAvailable,
173
+ remaining: data.weekly.remaining,
174
+ },
175
+ };
176
+ }
177
+ catch (error) {
178
+ // If API is unreachable, allow the request (graceful degradation)
179
+ return { allowed: true };
180
+ }
181
+ }
182
+ const FEATURE_LABELS = {
183
+ stealth: 'Stealth mode (anti-bot bypass)',
184
+ crawl: 'Crawl mode (multi-page)',
185
+ batch: 'Batch mode (bulk URLs)',
186
+ };
187
+ /**
188
+ * Check if user has access to a premium feature.
189
+ * Returns { allowed: true } for paid users, or a helpful upgrade message.
190
+ *
191
+ * Priority:
192
+ * 1. No API key → blocked (must sign up)
193
+ * 2. Has API key + cached plan → check plan tier
194
+ * 3. Has API key + no cache → check API, then cache
195
+ * 4. API unreachable + cached plan within 7 days → use cache
196
+ * 5. API unreachable + stale cache → allow gracefully (trust the user)
197
+ */
198
+ export async function checkFeatureAccess(feature) {
199
+ const config = loadConfig();
200
+ // No API key → must sign up and subscribe
201
+ if (!config.apiKey) {
202
+ return {
203
+ allowed: false,
204
+ message: `⚡ ${FEATURE_LABELS[feature]} requires a Pro plan ($9/mo).\n\n` +
205
+ ` Basic fetch is free — stealth, crawl, and batch are Pro features.\n\n` +
206
+ ` Sign up: https://app.webpeel.dev/signup\n` +
207
+ ` Pricing: https://webpeel.dev/#pricing\n` +
208
+ ` Login: webpeel login\n`,
209
+ };
210
+ }
211
+ // Try to get fresh plan info from API
212
+ try {
213
+ const response = await fetch(`${API_BASE_URL}/v1/usage`, {
214
+ headers: { 'Authorization': `Bearer ${config.apiKey}` },
215
+ signal: AbortSignal.timeout(5000),
216
+ });
217
+ if (response.ok) {
218
+ const data = await response.json();
219
+ const planName = (data.plan?.name || 'free').toLowerCase();
220
+ // Cache for offline use
221
+ config.planTier = planName;
222
+ config.planCachedAt = new Date().toISOString();
223
+ saveConfig(config);
224
+ if (planName === 'free') {
225
+ return {
226
+ allowed: false,
227
+ message: `⚡ ${FEATURE_LABELS[feature]} requires a Pro plan ($9/mo).\n\n` +
228
+ ` You're on the Free plan. Upgrade to unlock stealth, crawl, and batch.\n\n` +
229
+ ` Upgrade: https://app.webpeel.dev/billing\n` +
230
+ ` Pricing: https://webpeel.dev/#pricing\n`,
231
+ };
232
+ }
233
+ // Pro or Max — allowed
234
+ return { allowed: true };
235
+ }
236
+ }
237
+ catch {
238
+ // API unreachable — fall through to cache check
239
+ }
240
+ // API unreachable — use cached plan if recent (within 7 days)
241
+ if (config.planTier && config.planCachedAt) {
242
+ const cacheAge = Date.now() - new Date(config.planCachedAt).getTime();
243
+ const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
244
+ if (cacheAge < SEVEN_DAYS) {
245
+ if (config.planTier === 'free') {
246
+ return {
247
+ allowed: false,
248
+ message: `⚡ ${FEATURE_LABELS[feature]} requires a Pro plan ($9/mo).\n\n` +
249
+ ` Upgrade: https://app.webpeel.dev/billing\n`,
250
+ };
251
+ }
252
+ return { allowed: true };
253
+ }
254
+ }
255
+ // Stale cache or no cache, API unreachable — allow gracefully (trust paying users)
256
+ return { allowed: true };
257
+ }
258
+ /**
259
+ * Show usage footer after successful fetch (for free/anonymous users only)
260
+ */
261
+ export function showUsageFooter(usageInfo, isAnonymous, stealth = false) {
262
+ if (!usageInfo)
263
+ return;
264
+ // Only show footer for anonymous or free users
265
+ if (isAnonymous) {
266
+ const costText = stealth ? ' (costs 5 credits)' : '';
267
+ console.error(`⚡ ${usageInfo.remaining}/${usageInfo.limit} free fetches remaining${costText}. Run \`webpeel login\` to get 125/week free.`);
268
+ }
269
+ else if (usageInfo.limit <= 125) {
270
+ // Free tier authenticated users
271
+ const costText = stealth ? ' (costs 5 credits)' : '';
272
+ console.error(`⚡ ${usageInfo.remaining}/${usageInfo.limit} fetches remaining this week${costText}.`);
273
+ }
274
+ // Don't show footer for paid users
275
+ }
276
+ /**
277
+ * Prompt user for API key via stdin
278
+ */
279
+ export async function promptForApiKey() {
280
+ const rl = readline.createInterface({
281
+ input: process.stdin,
282
+ output: process.stdout,
283
+ });
284
+ return new Promise((resolve) => {
285
+ rl.question('Enter your API key (get one at https://app.webpeel.dev/keys): ', (answer) => {
286
+ rl.close();
287
+ resolve(answer.trim());
288
+ });
289
+ });
290
+ }
291
+ /**
292
+ * Login command - save API key to config
293
+ */
294
+ export async function handleLogin() {
295
+ const config = loadConfig();
296
+ if (config.apiKey) {
297
+ console.log('You are already logged in.');
298
+ console.log('Run `webpeel logout` first if you want to use a different API key.');
299
+ return;
300
+ }
301
+ console.log('\nWebPeel CLI Authentication');
302
+ console.log('==========================\n');
303
+ const apiKey = await promptForApiKey();
304
+ if (!apiKey) {
305
+ console.error('Error: API key cannot be empty');
306
+ process.exit(1);
307
+ }
308
+ // Validate API key format (should start with wp_)
309
+ if (!apiKey.startsWith('wp_')) {
310
+ console.error('Warning: API key should start with "wp_". Make sure you entered it correctly.');
311
+ }
312
+ // Save to config
313
+ config.apiKey = apiKey;
314
+ saveConfig(config);
315
+ console.log('\n✓ Successfully logged in!');
316
+ console.log('Run `webpeel usage` to check your quota.');
317
+ }
318
+ /**
319
+ * Logout command - remove API key from config
320
+ */
321
+ export function handleLogout() {
322
+ const config = loadConfig();
323
+ if (!config.apiKey) {
324
+ console.log('You are not logged in.');
325
+ return;
326
+ }
327
+ deleteConfig();
328
+ console.log('✓ Logged out successfully');
329
+ }
330
+ /**
331
+ * Usage command - show current quota
332
+ */
333
+ export async function handleUsage() {
334
+ const config = loadConfig();
335
+ // Check for weekly reset
336
+ if (shouldResetAnonymousUsage(config)) {
337
+ config.anonymousUsage = 0;
338
+ config.lastReset = getLastMonday().toISOString();
339
+ saveConfig(config);
340
+ }
341
+ // Anonymous user
342
+ if (!config.apiKey) {
343
+ const limit = 25;
344
+ const used = config.anonymousUsage;
345
+ const remaining = limit - used;
346
+ const nextMonday = new Date(getLastMonday());
347
+ nextMonday.setUTCDate(nextMonday.getUTCDate() + 7);
348
+ console.log('\nWebPeel Usage (Anonymous)');
349
+ console.log('=========================\n');
350
+ console.log(`Plan: Anonymous (25 free fetches/week)`);
351
+ console.log(`Used this week: ${used}/${limit}`);
352
+ console.log(`Remaining: ${remaining}`);
353
+ console.log(`Resets: ${nextMonday.toUTCString()}`);
354
+ console.log('\n💡 Run `webpeel login` to get 125 fetches/week for free!');
355
+ console.log(' Or sign up at https://app.webpeel.dev/signup\n');
356
+ return;
357
+ }
358
+ // Authenticated user - fetch from API
359
+ try {
360
+ const response = await fetch(`${API_BASE_URL}/v1/usage`, {
361
+ headers: {
362
+ 'Authorization': `Bearer ${config.apiKey}`,
363
+ },
364
+ });
365
+ if (!response.ok) {
366
+ if (response.status === 401) {
367
+ console.error('Error: Authentication failed. Your API key may be invalid.');
368
+ console.error('Run `webpeel logout` and `webpeel login` to re-authenticate.');
369
+ process.exit(1);
370
+ }
371
+ throw new Error(`API returned status ${response.status}`);
372
+ }
373
+ const data = await response.json();
374
+ console.log('\nWebPeel Usage');
375
+ console.log('=============\n');
376
+ console.log(`Plan: ${data.plan?.name || 'Free'} (${data.weekly.totalAvailable}/week)`);
377
+ console.log(`Used this week: ${data.weekly.totalUsed}/${data.weekly.totalAvailable}`);
378
+ console.log(`Remaining: ${data.weekly.remaining}`);
379
+ console.log(`Burst: ${data.session.burstUsed}/${data.session.burstLimit} (resets ${data.session.resetsIn || 'soon'})`);
380
+ console.log(`Resets: ${data.weekly.resetsAt}`);
381
+ if (data.weekly.remaining <= 10 && !data.extraUsage?.enabled) {
382
+ console.log('\n⚠️ Running low on credits. Upgrade at https://webpeel.dev/#pricing');
383
+ }
384
+ console.log();
385
+ }
386
+ catch (error) {
387
+ console.error(`Error fetching usage data: ${error instanceof Error ? error.message : 'Unknown error'}`);
388
+ console.error('The API may be temporarily unavailable. Try again later.');
389
+ process.exit(1);
390
+ }
391
+ }
392
+ //# sourceMappingURL=cli-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-auth.js","sourceRoot":"","sources":["../src/cli-auth.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -16,15 +16,17 @@ import { Command } from 'commander';
16
16
  import ora from 'ora';
17
17
  import { writeFileSync } from 'fs';
18
18
  import { peel, peelBatch, cleanup } from './index.js';
19
+ import { checkUsage, checkFeatureAccess, showUsageFooter, handleLogin, handleLogout, handleUsage } from './cli-auth.js';
19
20
  const program = new Command();
20
21
  program
21
22
  .name('webpeel')
22
23
  .description('Fast web fetcher for AI agents')
23
- .version('0.2.0')
24
+ .version('0.3.1')
24
25
  .enablePositionalOptions();
25
26
  program
26
27
  .argument('[url]', 'URL to fetch')
27
28
  .option('-r, --render', 'Use headless browser (for JS-heavy sites)')
29
+ .option('--stealth', 'Use stealth mode to bypass bot detection (auto-enables --render)')
28
30
  .option('-w, --wait <ms>', 'Wait time after page load (ms)', parseInt)
29
31
  .option('--html', 'Output raw HTML instead of markdown')
30
32
  .option('--text', 'Output plain text instead of markdown')
@@ -66,6 +68,21 @@ program
66
68
  console.error(`Error: Invalid URL format: ${url}`);
67
69
  process.exit(1);
68
70
  }
71
+ // Check premium feature access (stealth requires Pro plan)
72
+ const useStealth = options.stealth || false;
73
+ if (useStealth) {
74
+ const featureCheck = await checkFeatureAccess('stealth');
75
+ if (!featureCheck.allowed) {
76
+ console.error(featureCheck.message);
77
+ process.exit(1);
78
+ }
79
+ }
80
+ // Check usage quota
81
+ const usageCheck = await checkUsage();
82
+ if (!usageCheck.allowed) {
83
+ console.error(usageCheck.message);
84
+ process.exit(1);
85
+ }
69
86
  const spinner = options.silent ? null : ora('Fetching...').start();
70
87
  try {
71
88
  // Validate options
@@ -92,6 +109,7 @@ program
92
109
  // Build peel options
93
110
  const peelOptions = {
94
111
  render: options.render || false,
112
+ stealth: options.stealth || false,
95
113
  wait: options.wait || 0,
96
114
  timeout: options.timeout,
97
115
  userAgent: options.ua,
@@ -117,6 +135,10 @@ program
117
135
  if (spinner) {
118
136
  spinner.succeed(`Fetched in ${result.elapsed}ms using ${result.method} method`);
119
137
  }
138
+ // Show usage footer for free/anonymous users
139
+ if (usageCheck.usageInfo && !options.silent) {
140
+ showUsageFooter(usageCheck.usageInfo, usageCheck.isAnonymous || false, useStealth);
141
+ }
120
142
  // Handle screenshot saving
121
143
  if (options.screenshot && result.screenshot) {
122
144
  const screenshotPath = typeof options.screenshot === 'string'
@@ -183,6 +205,12 @@ program
183
205
  const isJson = options.json;
184
206
  const isSilent = options.silent;
185
207
  const count = parseInt(options.count) || 5;
208
+ // Check usage quota
209
+ const usageCheck = await checkUsage();
210
+ if (!usageCheck.allowed) {
211
+ console.error(usageCheck.message);
212
+ process.exit(1);
213
+ }
186
214
  const spinner = isSilent ? null : ora('Searching...').start();
187
215
  try {
188
216
  // Import the search function dynamically
@@ -241,6 +269,10 @@ program
241
269
  if (spinner) {
242
270
  spinner.succeed(`Found ${results.length} results`);
243
271
  }
272
+ // Show usage footer for free/anonymous users
273
+ if (usageCheck.usageInfo && !isSilent) {
274
+ showUsageFooter(usageCheck.usageInfo, usageCheck.isAnonymous || false, false);
275
+ }
244
276
  if (isJson) {
245
277
  const jsonStr = JSON.stringify(results, null, 2);
246
278
  await new Promise((resolve, reject) => {
@@ -277,7 +309,7 @@ program
277
309
  // Batch command
278
310
  program
279
311
  .command('batch <file>')
280
- .description('Fetch multiple URLs')
312
+ .description('Fetch multiple URLs (Pro feature)')
281
313
  .option('-c, --concurrency <n>', 'Max concurrent fetches (default: 3)', '3')
282
314
  .option('-o, --output <dir>', 'Output directory (one file per URL)')
283
315
  .option('--json', 'Output as JSON array')
@@ -289,6 +321,18 @@ program
289
321
  const isSilent = options.silent;
290
322
  const shouldRender = options.render;
291
323
  const selector = options.selector;
324
+ // Check premium feature access (batch requires Pro plan)
325
+ const featureCheck = await checkFeatureAccess('batch');
326
+ if (!featureCheck.allowed) {
327
+ console.error(featureCheck.message);
328
+ process.exit(1);
329
+ }
330
+ // Check usage quota
331
+ const usageCheck = await checkUsage();
332
+ if (!usageCheck.allowed) {
333
+ console.error(usageCheck.message);
334
+ process.exit(1);
335
+ }
292
336
  const spinner = isSilent ? null : ora('Loading URLs...').start();
293
337
  try {
294
338
  const { readFileSync } = await import('fs');
@@ -319,6 +363,10 @@ program
319
363
  const successCount = results.filter(r => 'content' in r).length;
320
364
  spinner.succeed(`Completed: ${successCount}/${urls.length} successful`);
321
365
  }
366
+ // Show usage footer for free/anonymous users
367
+ if (usageCheck.usageInfo && !isSilent) {
368
+ showUsageFooter(usageCheck.usageInfo, usageCheck.isAnonymous || false, false);
369
+ }
322
370
  // Output results
323
371
  if (isJson) {
324
372
  const jsonStr = JSON.stringify(results, null, 2);
@@ -380,6 +428,127 @@ program
380
428
  process.exit(1);
381
429
  }
382
430
  });
431
+ program
432
+ .command('crawl <url>')
433
+ .description('Crawl a website starting from a URL (Pro feature)')
434
+ .option('--max-pages <number>', 'Maximum number of pages to crawl (default: 10, max: 100)', parseInt, 10)
435
+ .option('--max-depth <number>', 'Maximum depth to crawl (default: 2, max: 5)', parseInt, 2)
436
+ .option('--allowed-domains <domains...>', 'Only crawl these domains (default: same as starting URL)')
437
+ .option('--exclude <patterns...>', 'Exclude URLs matching these regex patterns')
438
+ .option('--ignore-robots', 'Ignore robots.txt (default: respect robots.txt)')
439
+ .option('--rate-limit <ms>', 'Rate limit between requests in ms (default: 1000)', parseInt, 1000)
440
+ .option('-r, --render', 'Use headless browser for all pages')
441
+ .option('--stealth', 'Use stealth mode for all pages')
442
+ .option('-s, --silent', 'Silent mode (no spinner)')
443
+ .option('--json', 'Output as JSON')
444
+ .action(async (url, options) => {
445
+ // Check premium feature access (crawl requires Pro plan)
446
+ const featureCheck = await checkFeatureAccess('crawl');
447
+ if (!featureCheck.allowed) {
448
+ console.error(featureCheck.message);
449
+ process.exit(1);
450
+ }
451
+ // Check usage quota
452
+ const usageCheck = await checkUsage();
453
+ if (!usageCheck.allowed) {
454
+ console.error(usageCheck.message);
455
+ process.exit(1);
456
+ }
457
+ const { crawl } = await import('./core/crawler.js');
458
+ const spinner = options.silent ? null : ora('Crawling...').start();
459
+ try {
460
+ const results = await crawl(url, {
461
+ maxPages: options.maxPages,
462
+ maxDepth: options.maxDepth,
463
+ allowedDomains: options.allowedDomains,
464
+ excludePatterns: options.exclude,
465
+ respectRobotsTxt: !options.ignoreRobots,
466
+ rateLimitMs: options.rateLimit,
467
+ render: options.render || false,
468
+ stealth: options.stealth || false,
469
+ });
470
+ if (spinner) {
471
+ spinner.succeed(`Crawled ${results.length} pages`);
472
+ }
473
+ // Show usage footer for free/anonymous users
474
+ if (usageCheck.usageInfo && !options.silent) {
475
+ showUsageFooter(usageCheck.usageInfo, usageCheck.isAnonymous || false, options.stealth || false);
476
+ }
477
+ if (options.json) {
478
+ console.log(JSON.stringify(results, null, 2));
479
+ }
480
+ else {
481
+ results.forEach((result, i) => {
482
+ console.log(`\n${'='.repeat(60)}`);
483
+ console.log(`[${i + 1}/${results.length}] ${result.title}`);
484
+ console.log(`URL: ${result.url}`);
485
+ console.log(`Depth: ${result.depth}${result.parent ? ` (from: ${result.parent})` : ''}`);
486
+ console.log(`Links found: ${result.links.length}`);
487
+ console.log(`Elapsed: ${result.elapsed}ms`);
488
+ if (result.error) {
489
+ console.log(`ERROR: ${result.error}`);
490
+ }
491
+ else {
492
+ console.log(`\n${result.markdown.slice(0, 500)}${result.markdown.length > 500 ? '...' : ''}`);
493
+ }
494
+ });
495
+ }
496
+ await cleanup();
497
+ process.exit(0);
498
+ }
499
+ catch (error) {
500
+ if (spinner) {
501
+ spinner.fail('Crawl failed');
502
+ }
503
+ if (error instanceof Error) {
504
+ console.error(`\nError: ${error.message}`);
505
+ }
506
+ else {
507
+ console.error('\nError: Unknown error occurred');
508
+ }
509
+ await cleanup();
510
+ process.exit(1);
511
+ }
512
+ });
513
+ program
514
+ .command('login')
515
+ .description('Authenticate the CLI with your API key')
516
+ .action(async () => {
517
+ try {
518
+ await handleLogin();
519
+ process.exit(0);
520
+ }
521
+ catch (error) {
522
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
523
+ process.exit(1);
524
+ }
525
+ });
526
+ program
527
+ .command('logout')
528
+ .description('Clear your saved credentials')
529
+ .action(() => {
530
+ try {
531
+ handleLogout();
532
+ process.exit(0);
533
+ }
534
+ catch (error) {
535
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
536
+ process.exit(1);
537
+ }
538
+ });
539
+ program
540
+ .command('usage')
541
+ .description('Show your current usage and quota')
542
+ .action(async () => {
543
+ try {
544
+ await handleUsage();
545
+ process.exit(0);
546
+ }
547
+ catch (error) {
548
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
549
+ process.exit(1);
550
+ }
551
+ });
383
552
  program
384
553
  .command('serve')
385
554
  .description('Start API server')