vibex-sh 0.10.1 → 0.11.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.
Files changed (2) hide show
  1. package/index.js +360 -147
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -11,6 +11,14 @@ import { fileURLToPath } from 'url';
11
11
  import WebSocket from 'ws';
12
12
  import crypto from 'crypto';
13
13
 
14
+ // Constants
15
+ const POLL_INTERVAL_MS = 1000;
16
+ const MAX_POLL_ATTEMPTS = 60;
17
+ const WEBSOCKET_CLOSE_TIMEOUT_MS = 2000;
18
+ const DEFAULT_WORKER_URL = 'https://ingest.vibex.sh';
19
+ const DEFAULT_WEB_URL = 'https://vibex.sh';
20
+ const PIPED_INPUT_DELAY_MS = parseInt(process.env.VIBEX_PIPE_DELAY_MS || '300', 10);
21
+
14
22
  // Get version from package.json
15
23
  const __filename = fileURLToPath(import.meta.url);
16
24
  const __dirname = dirname(__filename);
@@ -153,45 +161,16 @@ function normalizeToHybrid(message, level, payload) {
153
161
  return hybrid;
154
162
  }
155
163
 
156
- function deriveSocketUrl(webUrl) {
157
- // Always use production worker WebSocket endpoint
158
- const workerUrl = process.env.VIBEX_WORKER_URL || 'https://ingest.vibex.sh';
159
- return workerUrl.replace('https://', 'wss://').replace('http://', 'ws://');
160
- }
161
-
162
- function getUrls(options) {
163
- const { web, socket, server } = options;
164
-
165
- // Priority 1: Explicit --web and --socket flags (highest priority)
166
- if (web) {
167
- return {
168
- webUrl: web,
169
- socketUrl: socket || deriveSocketUrl(web),
170
- };
171
- }
172
-
173
- // Priority 2: --server flag (shorthand for --web)
174
- if (server) {
175
- return {
176
- webUrl: server,
177
- socketUrl: socket || deriveSocketUrl(server),
178
- };
179
- }
180
-
181
- // Priority 3: Environment variables
182
- if (process.env.VIBEX_WEB_URL) {
183
- return {
184
- webUrl: process.env.VIBEX_WEB_URL,
185
- socketUrl: process.env.VIBEX_SOCKET_URL || socket || deriveSocketUrl(process.env.VIBEX_WEB_URL),
186
- };
187
- }
188
-
189
- // Priority 4: Production defaults
164
+ /**
165
+ * Get production URLs
166
+ * CLI only supports production server
167
+ */
168
+ function getProductionUrls() {
190
169
  // Always use production worker WebSocket endpoint
191
- const defaultWorkerUrl = process.env.VIBEX_WORKER_URL || 'https://ingest.vibex.sh';
170
+ const workerUrl = process.env.VIBEX_WORKER_URL || DEFAULT_WORKER_URL;
192
171
  return {
193
- webUrl: 'https://vibex.sh',
194
- socketUrl: socket || defaultWorkerUrl.replace('https://', 'wss://').replace('http://', 'ws://'),
172
+ webUrl: DEFAULT_WEB_URL,
173
+ socketUrl: workerUrl.replace('https://', 'wss://').replace('http://', 'ws://'),
195
174
  };
196
175
  }
197
176
 
@@ -230,7 +209,7 @@ function getStoredConfig() {
230
209
  return null;
231
210
  }
232
211
 
233
- async function storeToken(token, webUrl = null) {
212
+ async function storeToken(token) {
234
213
  try {
235
214
  const configPath = getConfigPath();
236
215
  const configDir = join(homedir(), '.vibex');
@@ -240,7 +219,6 @@ async function storeToken(token, webUrl = null) {
240
219
 
241
220
  const config = {
242
221
  token,
243
- ...(webUrl && { webUrl }), // Store webUrl if provided
244
222
  updatedAt: new Date().toISOString(),
245
223
  };
246
224
 
@@ -252,9 +230,10 @@ async function storeToken(token, webUrl = null) {
252
230
  }
253
231
  }
254
232
 
255
- async function handleLogin(webUrl) {
233
+ async function handleLogin() {
256
234
  const configPath = getConfigPath();
257
235
  const existingConfig = getStoredConfig();
236
+ const { webUrl } = getProductionUrls();
258
237
 
259
238
  console.log('\n 🔐 vibex.sh CLI Authentication\n');
260
239
  console.log(` 📁 Config location: ${configPath}`);
@@ -263,8 +242,9 @@ async function handleLogin(webUrl) {
263
242
  console.log(` ⚠️ You already have a token stored. This will replace it.\n`);
264
243
  }
265
244
 
266
- const tempToken = `temp_${Date.now()}_${Math.random().toString(36).substring(7)}`;
267
- const authUrl = `${webUrl}/api/cli-auth?token=${tempToken}`;
245
+ // Generate unique state (nonce) for OAuth flow
246
+ const state = crypto.randomBytes(16).toString('hex');
247
+ const authUrl = `${webUrl}/api/cli-auth?state=${state}`;
268
248
 
269
249
  console.log(' Opening browser for authentication...\n');
270
250
  console.log(` If browser doesn't open, visit: ${authUrl}\n`);
@@ -284,21 +264,20 @@ async function handleLogin(webUrl) {
284
264
 
285
265
  // Poll for token
286
266
  console.log(' Waiting for authentication...');
287
- const maxAttempts = 60; // 60 seconds
288
267
  let attempts = 0;
289
268
 
290
- while (attempts < maxAttempts) {
291
- await new Promise(resolve => setTimeout(resolve, 1000));
269
+ while (attempts < MAX_POLL_ATTEMPTS) {
270
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
292
271
  attempts++;
293
272
 
294
273
  try {
295
- const response = await httpRequest(`${webUrl}/api/cli-auth?token=${tempToken}`, {
274
+ const response = await httpRequest(`${webUrl}/api/cli-auth?state=${state}`, {
296
275
  method: 'GET',
297
276
  });
298
277
  if (response.ok) {
299
278
  const data = await response.json();
300
279
  if (data.success && data.token) {
301
- await storeToken(data.token, webUrl);
280
+ await storeToken(data.token);
302
281
  const configPath = getConfigPath();
303
282
  console.log('\n ✅ Authentication successful!');
304
283
  console.log(` 📁 Token saved to: ${configPath}`);
@@ -342,39 +321,16 @@ function httpRequest(url, options) {
342
321
  });
343
322
  }
344
323
 
345
- async function claimSession(sessionId, token, webUrl) {
346
- if (!token) return null; // Return null instead of false to indicate no claim attempted
347
-
348
- try {
349
- // Normalize session ID before claiming
350
- const normalizedSessionId = normalizeSessionId(sessionId);
351
- const response = await httpRequest(`${webUrl}/api/auth/claim-session-with-token`, {
352
- method: 'POST',
353
- headers: { 'Content-Type': 'application/json' },
354
- body: JSON.stringify({
355
- sessionId: normalizedSessionId,
356
- token,
357
- }),
358
- });
359
-
360
- if (response.ok) {
361
- // Parse response to get auth code
362
- const responseData = await response.json();
363
- return responseData.authCode || null;
364
- }
365
-
366
- return null;
367
- } catch (error) {
368
- return null;
369
- }
370
- }
324
+ // claimSession function removed - session claiming is no longer supported
325
+ // All sessions must be created with authentication
371
326
 
372
327
  // Removed getSessionAuthCode - auth codes should only come from:
373
328
  // 1. claim-session-with-token response (for claimed sessions)
374
329
  // 2. socket.io session-auth-code event (for unclaimed sessions)
375
330
  // Never fetch auth codes via public API endpoint - security vulnerability
376
331
 
377
- function printBanner(sessionId, webUrl, authCode = null) {
332
+ function printBanner(sessionId, authCode = null) {
333
+ const { webUrl } = getProductionUrls();
378
334
  const dashboardUrl = authCode
379
335
  ? `${webUrl}/${sessionId}?auth=${authCode}`
380
336
  : `${webUrl}/${sessionId}`;
@@ -392,88 +348,273 @@ function printBanner(sessionId, webUrl, authCode = null) {
392
348
  console.log('\n');
393
349
  }
394
350
 
395
- async function main() {
396
- // Handle --version flag early (before commander parses)
397
- const allArgs = process.argv;
398
- const args = process.argv.slice(2);
351
+ /**
352
+ * Handle init command - create a new session with parser selection
353
+ */
354
+ async function handleInit(options) {
355
+ const { webUrl } = getProductionUrls();
399
356
 
400
- // Check for --version or -V flag
401
- if (allArgs.includes('--version') || allArgs.includes('-V') || args.includes('--version') || args.includes('-V')) {
402
- console.log(cliVersion);
403
- process.exit(0);
404
- }
357
+ // Interactive prompt for parser selection
358
+ const rl = readline.createInterface({
359
+ input: process.stdin,
360
+ output: process.stdout,
361
+ });
405
362
 
406
- // Handle login command separately - check BEFORE commander parses
407
- // Check process.argv directly - look for 'login' as a standalone argument
408
- // This must happen FIRST, before any commander parsing
409
- // Check if 'login' appears anywhere in process.argv (works with npx too)
410
- const hasLogin = allArgs.includes('login') || args.includes('login');
363
+ const question = (query) => new Promise((resolve) => rl.question(query, resolve));
411
364
 
412
- if (hasLogin) {
413
- // Find login position to get args after it
414
- const loginIndex = args.indexOf('login');
415
- const loginArgs = loginIndex !== -1 ? args.slice(loginIndex + 1) : [];
416
-
417
- // Create a separate command instance for login
418
- const loginCmd = new Command();
419
- loginCmd
420
- .option('--web <url>', 'Web server URL')
421
- .option('--server <url>', 'Shorthand for --web');
422
-
423
- // Parse only the options (args after 'login')
424
- if (loginArgs.length > 0) {
425
- loginCmd.parse(['node', 'vibex', ...loginArgs], { from: 'user' });
365
+ try {
366
+ console.log('\n 🔧 vibex.sh Session Initialization\n');
367
+
368
+ // Fetch available parsers from public API
369
+ let availableParsers = [];
370
+ try {
371
+ const parsersResponse = await httpRequest(`${webUrl}/api/parsers`, {
372
+ method: 'GET',
373
+ });
374
+ if (parsersResponse.ok) {
375
+ availableParsers = await parsersResponse.json();
376
+ } else {
377
+ console.warn(' ⚠️ Failed to fetch parsers from API, using fallback list');
378
+ }
379
+ } catch (e) {
380
+ console.warn(' ⚠️ Failed to fetch parsers from API, using fallback list');
381
+ }
382
+
383
+ // Fallback to hardcoded list if API fails or returns empty
384
+ if (!availableParsers || availableParsers.length === 0) {
385
+ availableParsers = [
386
+ { id: 'nginx', name: 'Nginx Access Log', category: 'web' },
387
+ { id: 'apache', name: 'Apache Access Log', category: 'web' },
388
+ { id: 'docker', name: 'Docker Container Logs', category: 'system' },
389
+ { id: 'kubernetes', name: 'Kubernetes Pod/Container Logs', category: 'system' },
390
+ ];
391
+ }
392
+
393
+ // Mandatory parsers that are always included
394
+ const mandatoryParserIds = ['json-in-text', 'raw', 'keyvalue', 'stacktrace', 'smart-pattern'];
395
+
396
+ // Filter out mandatory parsers for selection (only optional parsers can be selected)
397
+ const selectableParsers = availableParsers.filter(p => !mandatoryParserIds.includes(p.id) && !p.isMandatory);
398
+
399
+ // Pre-select mandatory parsers
400
+ let enabledParsers = [...mandatoryParserIds];
401
+
402
+ console.log(' Mandatory parsers are automatically included:');
403
+ mandatoryParserIds.forEach(id => {
404
+ const parser = availableParsers.find(p => p.id === id);
405
+ if (parser) {
406
+ console.log(` ✓ ${parser.name}`);
407
+ }
408
+ });
409
+ console.log('');
410
+
411
+ console.log(' Select additional optional parsers (leave empty for mandatory only):');
412
+ if (selectableParsers.length > 0) {
413
+ console.log(' Available optional parsers:');
414
+ selectableParsers.forEach((p, i) => {
415
+ console.log(` ${i + 1}. ${p.name} (${p.id})`);
416
+ });
417
+ console.log(' (Leave empty for mandatory parsers only)\n');
426
418
  } else {
427
- loginCmd.parse(['node', 'vibex'], { from: 'user' });
419
+ console.log(' No additional optional parsers available.\n');
420
+ }
421
+
422
+ const answer = await question(' Enter comma-separated numbers or parser IDs (e.g., 1,2 or docker,kubernetes): ');
423
+ rl.close();
424
+
425
+ if (answer.trim()) {
426
+ const selections = answer.split(',').map(s => s.trim());
427
+ selections.forEach(sel => {
428
+ // Check if it's a number
429
+ const num = parseInt(sel, 10);
430
+ if (!isNaN(num) && num > 0 && num <= selectableParsers.length) {
431
+ enabledParsers.push(selectableParsers[num - 1].id);
432
+ } else if (selectableParsers.find(p => p.id === sel)) {
433
+ enabledParsers.push(sel);
434
+ }
435
+ });
436
+ }
437
+
438
+ // Use parser flag if provided, otherwise use interactive selection
439
+ const parserFlag = options.parser || options.parsers;
440
+ if (parserFlag) {
441
+ if (typeof parserFlag === 'string') {
442
+ // Add optional parsers from flag, but always include mandatory
443
+ const flagParsers = parserFlag.split(',').map(p => p.trim());
444
+ enabledParsers = [...new Set([...mandatoryParserIds, ...flagParsers])];
445
+ }
446
+ }
447
+
448
+ // Get token - required for authenticated session creation
449
+ let token = process.env.VIBEX_TOKEN || await getStoredToken();
450
+ if (!token) {
451
+ console.error('\n ✗ Authentication required');
452
+ console.error(' 💡 Run: npx vibex-sh login');
453
+ process.exit(1);
454
+ }
455
+
456
+ // Create session with enabledParsers (authenticated)
457
+ const createUrl = `${webUrl}/api/sessions/create`;
458
+ const response = await httpRequest(createUrl, {
459
+ method: 'POST',
460
+ headers: {
461
+ 'Content-Type': 'application/json',
462
+ 'Authorization': `Bearer ${token}`,
463
+ },
464
+ body: JSON.stringify({
465
+ enabledParsers: enabledParsers.length > 0 ? enabledParsers : undefined,
466
+ }),
467
+ });
468
+
469
+ if (!response.ok) {
470
+ const errorData = await response.json();
471
+ if (response.status === 401 || response.status === 403) {
472
+ console.error(`\n ✗ Authentication failed: ${errorData.message || 'Invalid token'}`);
473
+ console.error(' 💡 Run: npx vibex-sh login');
474
+ } else {
475
+ console.error(`\n ✗ Failed to create session: ${errorData.message || 'Unknown error'}`);
476
+ }
477
+ process.exit(1);
478
+ }
479
+
480
+ const data = await response.json();
481
+ const createdSessionId = data.sessionId;
482
+ const createdAuthCode = data.authCode;
483
+
484
+ console.log('\n ✅ Session created successfully!\n');
485
+ printBanner(createdSessionId, createdAuthCode);
486
+
487
+ // Separate mandatory and optional parsers for display
488
+ const mandatoryIncluded = enabledParsers.filter(id => mandatoryParserIds.includes(id));
489
+ const optionalIncluded = enabledParsers.filter(id => !mandatoryParserIds.includes(id));
490
+
491
+ if (mandatoryIncluded.length > 0) {
492
+ console.log(` 📋 Mandatory parsers: ${mandatoryIncluded.join(', ')}`);
493
+ }
494
+ if (optionalIncluded.length > 0) {
495
+ console.log(` 📋 Optional parsers: ${optionalIncluded.join(', ')}`);
496
+ } else if (mandatoryIncluded.length === mandatoryParserIds.length) {
497
+ console.log(' 📋 Using mandatory parsers only');
428
498
  }
499
+ console.log(`\n 💡 Use this session ID: ${createdSessionId}`);
500
+ console.log(` Example: echo '{"cpu": 45}' | npx vibex-sh -s ${createdSessionId}\n`);
429
501
 
430
- const options = loginCmd.opts();
431
- const { webUrl } = getUrls(options);
432
- await handleLogin(webUrl);
433
502
  process.exit(0);
503
+ } catch (error) {
504
+ rl.close();
505
+ console.error(`\n ✗ Error: ${error.message}`);
506
+ process.exit(1);
434
507
  }
508
+ }
509
+
510
+ async function main() {
511
+ // Configure main program
512
+ program
513
+ .name('vibex')
514
+ .description('vibex.sh CLI - Send logs to vibex.sh for real-time analysis')
515
+ .version(cliVersion, '-v, --version', 'Display version number');
516
+
517
+ // Login command
518
+ program
519
+ .command('login')
520
+ .description('Authenticate with vibex.sh and save your token')
521
+ .action(async () => {
522
+ await handleLogin();
523
+ process.exit(0);
524
+ });
525
+
526
+ // Init command
527
+ program
528
+ .command('init')
529
+ .description('Create a new session with parser selection')
530
+ .option('--parser <parsers>', 'Comma-separated list of parser IDs (e.g., nginx,postgres)')
531
+ .option('--parsers <parsers>', 'Alias for --parser')
532
+ .action(async (options) => {
533
+ await handleInit(options);
534
+ });
435
535
 
536
+ // Main command (default) - send logs
436
537
  program
437
- .version(cliVersion, '-v, --version', 'Display version number')
438
538
  .option('-s, --session-id <id>', 'Reuse existing session ID')
439
- .option('--web <url>', 'Web server URL')
440
- .option('--socket <url>', 'Socket server URL')
441
- .option('--server <url>', 'Shorthand for --web (auto-derives socket URL)')
442
539
  .option('--token <token>', 'Authentication token (or use VIBEX_TOKEN env var)')
443
- .parse();
540
+ .option('--parser <parsers>', 'Comma-separated list of parser IDs (e.g., nginx,postgres)')
541
+ .option('--parsers <parsers>', 'Alias for --parser')
542
+ .action(async (options) => {
543
+ await handleSendLogs(options);
544
+ });
545
+
546
+ // Parse arguments
547
+ program.parse();
548
+ }
444
549
 
445
- const options = program.opts();
446
- const { webUrl, socketUrl } = getUrls(options);
550
+ /**
551
+ * Handle send logs command (default/main command)
552
+ */
553
+ async function handleSendLogs(options) {
554
+ const { webUrl, socketUrl } = getProductionUrls();
447
555
 
448
- // Get token from flag, env var, or stored config
556
+ // Get token - REQUIRED for all operations
449
557
  let token = options.token || process.env.VIBEX_TOKEN || await getStoredToken();
558
+ if (!token) {
559
+ console.error('\n ✗ Authentication required');
560
+ console.error(' 💡 Run: npx vibex-sh login to authenticate');
561
+ console.error(' 💡 Or set VIBEX_TOKEN environment variable\n');
562
+ process.exit(1);
563
+ }
450
564
 
451
565
  let sessionId;
452
566
  let authCode = null;
453
567
 
568
+ // Check if stdin is available (piped input)
569
+ const isTTY = process.stdin.isTTY;
570
+ const hasStdin = !isTTY;
571
+
572
+ // If no session ID and no stdin, show usage and exit
573
+ if (!options.sessionId && !hasStdin) {
574
+ program.help();
575
+ process.exit(0);
576
+ }
577
+
454
578
  // If session ID is provided, use it (existing session)
455
579
  if (options.sessionId) {
456
580
  sessionId = normalizeSessionId(options.sessionId);
457
581
 
458
- // If token is available, try to claim the session
459
- if (token) {
460
- authCode = await claimSession(sessionId, token, webUrl);
461
- }
462
-
463
582
  // When reusing a session, show minimal info
464
583
  console.log(` 🔍 Sending logs to session: ${sessionId}\n`);
465
584
  } else {
466
- // No session ID provided - create a new anonymous session
585
+ // No session ID provided - create a new authenticated session
586
+ // Check for --parser or --parsers flag for parser selection
587
+ let enabledParsers = [];
588
+ if (options.parser || options.parsers) {
589
+ const parserList = options.parser || options.parsers;
590
+ if (Array.isArray(parserList)) {
591
+ enabledParsers = parserList;
592
+ } else if (typeof parserList === 'string') {
593
+ enabledParsers = parserList.split(',').map(p => p.trim());
594
+ }
595
+ }
596
+
467
597
  try {
468
- const createUrl = `${webUrl}/api/sessions/create-anonymous`;
598
+ const createUrl = `${webUrl}/api/sessions/create`;
469
599
  const response = await httpRequest(createUrl, {
470
600
  method: 'POST',
471
- headers: { 'Content-Type': 'application/json' },
601
+ headers: {
602
+ 'Content-Type': 'application/json',
603
+ 'Authorization': `Bearer ${token}`,
604
+ },
605
+ body: JSON.stringify({
606
+ enabledParsers: enabledParsers.length > 0 ? enabledParsers : undefined,
607
+ }),
472
608
  });
473
609
 
474
610
  if (!response.ok) {
475
611
  const errorData = await response.json();
476
- console.error(` ✗ Failed to create session: ${errorData.message || 'Unknown error'}`);
612
+ if (response.status === 401 || response.status === 403) {
613
+ console.error(` ✗ Authentication failed: ${errorData.message || 'Invalid token'}`);
614
+ console.error(' 💡 Run: npx vibex-sh login');
615
+ } else {
616
+ console.error(` ✗ Failed to create session: ${errorData.message || 'Unknown error'}`);
617
+ }
477
618
  process.exit(1);
478
619
  }
479
620
 
@@ -481,17 +622,13 @@ async function main() {
481
622
  sessionId = data.sessionId; // Server-generated unique session ID
482
623
  authCode = data.authCode; // Server-generated auth code
483
624
 
484
- // If token is available, claim the session
485
- if (token) {
486
- const claimAuthCode = await claimSession(sessionId, token, webUrl);
487
- if (claimAuthCode) {
488
- authCode = claimAuthCode;
489
- console.log(' ✓ Session automatically claimed to your account\n');
490
- }
491
- }
492
-
493
625
  // Print banner for new session
494
- printBanner(sessionId, webUrl, authCode);
626
+ printBanner(sessionId, authCode);
627
+ if (enabledParsers.length > 0) {
628
+ console.log(` 📋 Log Types: ${enabledParsers.join(', ')}`);
629
+ } else {
630
+ console.log(' 📋 Log Types: Auto-detection (default parsers)');
631
+ }
495
632
  console.log(' 💡 Tip: Use -s to send more logs to this session');
496
633
  console.log(` Example: echo '{"cpu": 45, "memory": 78}' | npx vibex-sh -s ${sessionId}\n`);
497
634
  } catch (error) {
@@ -639,6 +776,7 @@ async function main() {
639
776
  if (!receivedAuthCode || receivedAuthCode !== message.data.authCode) {
640
777
  receivedAuthCode = message.data.authCode;
641
778
  if (isNewSession) {
779
+ const { webUrl } = getProductionUrls();
642
780
  console.log(` 🔑 Auth Code: ${receivedAuthCode}`);
643
781
  console.log(` 📋 Dashboard: ${webUrl}/${sessionId}?auth=${receivedAuthCode}\n`);
644
782
  }
@@ -798,21 +936,19 @@ async function main() {
798
936
 
799
937
  // Send logs via HTTP POST (non-blocking, same as SDKs)
800
938
  // Always use production Cloudflare Worker endpoint
801
- // Token is optional - anonymous sessions can send logs without authentication
939
+ // Token is REQUIRED - all sessions must be authenticated
802
940
  // HTTP POST works independently of WebSocket - don't wait for WebSocket connection
803
941
  const sendLogViaHTTP = async (logData) => {
804
942
  try {
805
943
  // Always use production worker URL
806
- const workerUrl = process.env.VIBEX_WORKER_URL || 'https://ingest.vibex.sh';
944
+ const workerUrl = process.env.VIBEX_WORKER_URL || DEFAULT_WORKER_URL;
807
945
  const ingestUrl = `${workerUrl}/api/v1/ingest`;
808
946
 
809
- // Build headers - only include Authorization if token exists
947
+ // Build headers - Authorization is REQUIRED
810
948
  const headers = {
811
949
  'Content-Type': 'application/json',
950
+ 'Authorization': `Bearer ${token}`,
812
951
  };
813
- if (token) {
814
- headers['Authorization'] = `Bearer ${token}`;
815
- }
816
952
 
817
953
  const response = await fetch(ingestUrl, {
818
954
  method: 'POST',
@@ -913,8 +1049,67 @@ async function main() {
913
1049
  }
914
1050
  };
915
1051
 
916
- // Start WebSocket connection
917
- connectWebSocket();
1052
+ // Only start WebSocket connection if we have stdin (piped input)
1053
+ // Don't connect WebSocket when run without parameters
1054
+ if (hasStdin) {
1055
+ connectWebSocket();
1056
+ }
1057
+
1058
+ // Only read from stdin if we have piped input
1059
+ if (!hasStdin) {
1060
+ // No stdin - exit after showing session info
1061
+ process.exit(0);
1062
+ }
1063
+
1064
+ // Rate limiting queue for piped input
1065
+ const pipedLogQueue = [];
1066
+ let isProcessingQueue = false;
1067
+ let queueProcessingTimeout = null;
1068
+
1069
+ const processPipedLogQueue = async () => {
1070
+ if (isProcessingQueue) {
1071
+ return;
1072
+ }
1073
+
1074
+ if (pipedLogQueue.length === 0) {
1075
+ return;
1076
+ }
1077
+
1078
+ isProcessingQueue = true;
1079
+
1080
+ try {
1081
+ while (pipedLogQueue.length > 0) {
1082
+ const logData = pipedLogQueue.shift();
1083
+ await sendLogViaHTTP(logData);
1084
+
1085
+ // Wait before sending the next log (only if there are more logs in queue)
1086
+ if (pipedLogQueue.length > 0) {
1087
+ await new Promise(resolve => setTimeout(resolve, PIPED_INPUT_DELAY_MS));
1088
+ }
1089
+ }
1090
+ } finally {
1091
+ isProcessingQueue = false;
1092
+ }
1093
+ };
1094
+
1095
+ const queuePipedLog = (logData) => {
1096
+ pipedLogQueue.push(logData);
1097
+
1098
+ // Start processing if not already processing
1099
+ if (!isProcessingQueue) {
1100
+ // Clear any existing timeout
1101
+ if (queueProcessingTimeout) {
1102
+ clearTimeout(queueProcessingTimeout);
1103
+ queueProcessingTimeout = null;
1104
+ }
1105
+ // Process queue with a small initial delay to batch rapid inputs
1106
+ queueProcessingTimeout = setTimeout(() => {
1107
+ processPipedLogQueue().catch((error) => {
1108
+ console.error(' ✗ Error processing log queue:', error.message);
1109
+ });
1110
+ }, 10);
1111
+ }
1112
+ };
918
1113
 
919
1114
  const rl = readline.createInterface({
920
1115
  input: process.stdin,
@@ -966,13 +1161,29 @@ async function main() {
966
1161
  };
967
1162
  }
968
1163
 
969
- // Send logs via HTTP POST immediately - don't wait for WebSocket
970
- // WebSocket is only for receiving logs and auth codes, not required for sending
971
- sendLogViaHTTP(logData);
1164
+ // Queue logs for rate-limited sending when input is piped
1165
+ // This prevents overwhelming rate limits when piping large files
1166
+ queuePipedLog(logData);
972
1167
  });
973
1168
 
974
1169
  rl.on('close', async () => {
975
- // Wait for queued logs to be sent
1170
+ // Clear any pending queue processing timeout
1171
+ if (queueProcessingTimeout) {
1172
+ clearTimeout(queueProcessingTimeout);
1173
+ queueProcessingTimeout = null;
1174
+ }
1175
+
1176
+ // Process any remaining logs in the piped queue
1177
+ // Keep processing until queue is empty and not currently processing
1178
+ while (pipedLogQueue.length > 0 || isProcessingQueue) {
1179
+ await processPipedLogQueue();
1180
+ // Small delay to allow processing to complete
1181
+ if (pipedLogQueue.length > 0 || isProcessingQueue) {
1182
+ await new Promise(resolve => setTimeout(resolve, 50));
1183
+ }
1184
+ }
1185
+
1186
+ // Wait for queued logs to be sent (WebSocket queue)
976
1187
  const waitForQueue = () => {
977
1188
  return new Promise((resolve) => {
978
1189
  if (logQueue.length === 0) {
@@ -993,8 +1204,10 @@ async function main() {
993
1204
  reconnectTimeout = null;
994
1205
  }
995
1206
 
996
- // Graceful shutdown - wait for close handshake
997
- await closeWebSocket();
1207
+ // Graceful shutdown - wait for close handshake (only if WebSocket was connected)
1208
+ if (hasStdin && socket) {
1209
+ await closeWebSocket();
1210
+ }
998
1211
 
999
1212
  // Give a moment for any final cleanup
1000
1213
  setTimeout(() => process.exit(0), 100);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibex-sh",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Zero-config observability CLI - pipe logs and visualize instantly",
5
5
  "type": "module",
6
6
  "bin": {