vibex-sh 0.9.9 → 0.10.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.
- package/README.md +10 -29
- package/index.js +155 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,12 +7,6 @@ Zero-config observability CLI - pipe logs and visualize instantly.
|
|
|
7
7
|
```bash
|
|
8
8
|
# Production (default)
|
|
9
9
|
echo '{"cpu": 45, "memory": 78}' | vibex
|
|
10
|
-
|
|
11
|
-
# Local development
|
|
12
|
-
echo '{"test": 123}' | vibex --local
|
|
13
|
-
|
|
14
|
-
# Custom ports
|
|
15
|
-
echo '{"data": 123}' | vibex --web http://localhost:3000 --socket http://localhost:8080
|
|
16
10
|
```
|
|
17
11
|
|
|
18
12
|
## Installation
|
|
@@ -43,40 +37,33 @@ echo '{"more": "data"}' | vibex --session-id vibex-abc123
|
|
|
43
37
|
| Flag | Description | Example |
|
|
44
38
|
|------|-------------|---------|
|
|
45
39
|
| `-s, --session-id <id>` | Reuse existing session | `vibex --session-id vibex-abc123` |
|
|
46
|
-
|
|
|
47
|
-
| `--
|
|
48
|
-
| `--
|
|
49
|
-
| `--server <url>` | Shorthand for `--web` (auto-derives socket) | `vibex --server http://localhost:3000` |
|
|
40
|
+
| `--web <url>` | Web server URL | `vibex --web https://vibex.sh` |
|
|
41
|
+
| `--socket <url>` | Socket server URL | `vibex --socket wss://ingest.vibex.sh` |
|
|
42
|
+
| `--server <url>` | Shorthand for `--web` (auto-derives socket) | `vibex --server https://vibex.sh` |
|
|
50
43
|
|
|
51
44
|
## Server Configuration
|
|
52
45
|
|
|
53
46
|
The CLI automatically derives the socket URL from the web URL, but you can override it:
|
|
54
47
|
|
|
55
48
|
```bash
|
|
56
|
-
#
|
|
57
|
-
vibex --web http://localhost:3000
|
|
58
|
-
|
|
59
|
-
# Explicit socket URL
|
|
60
|
-
vibex --web http://localhost:3000 --socket http://localhost:8080
|
|
61
|
-
|
|
62
|
-
# Production (auto-derives socket.vibex.sh)
|
|
49
|
+
# Production (auto-derives socket URL)
|
|
63
50
|
vibex --server https://vibex.sh
|
|
64
51
|
|
|
65
52
|
# Custom domain
|
|
66
|
-
vibex --web https://staging.vibex.sh --socket
|
|
53
|
+
vibex --web https://staging.vibex.sh --socket wss://ingest-staging.vibex.sh
|
|
67
54
|
```
|
|
68
55
|
|
|
69
56
|
## Priority Order
|
|
70
57
|
|
|
71
|
-
1. **Flags** (`--web`, `--socket`, `--
|
|
58
|
+
1. **Flags** (`--web`, `--socket`, `--server`)
|
|
72
59
|
2. **Environment variables** (`VIBEX_WEB_URL`, `VIBEX_SOCKET_URL`)
|
|
73
|
-
3. **Production defaults** (`https://vibex.sh`, `
|
|
60
|
+
3. **Production defaults** (`https://vibex.sh`, `wss://ingest.vibex.sh`)
|
|
74
61
|
|
|
75
62
|
## Environment Variables
|
|
76
63
|
|
|
77
64
|
```bash
|
|
78
|
-
export VIBEX_WEB_URL=
|
|
79
|
-
export VIBEX_SOCKET_URL=
|
|
65
|
+
export VIBEX_WEB_URL=https://vibex.sh
|
|
66
|
+
export VIBEX_SOCKET_URL=wss://ingest.vibex.sh
|
|
80
67
|
```
|
|
81
68
|
|
|
82
69
|
## Examples
|
|
@@ -85,14 +72,8 @@ export VIBEX_SOCKET_URL=http://localhost:8080
|
|
|
85
72
|
# Production (default)
|
|
86
73
|
echo '{"data": 123}' | vibex
|
|
87
74
|
|
|
88
|
-
# Quick localhost
|
|
89
|
-
echo '{"data": 123}' | vibex --local
|
|
90
|
-
|
|
91
75
|
# Custom web server, auto socket
|
|
92
|
-
echo '{"data": 123}' | vibex --server
|
|
93
|
-
|
|
94
|
-
# Both custom
|
|
95
|
-
echo '{"data": 123}' | vibex --web http://localhost:3000 --socket http://localhost:8080
|
|
76
|
+
echo '{"data": 123}' | vibex --server https://vibex.sh
|
|
96
77
|
|
|
97
78
|
# Staging
|
|
98
79
|
echo '{"data": 123}' | vibex --server https://staging.vibex.sh
|
package/index.js
CHANGED
|
@@ -56,28 +56,111 @@ function normalizeSessionId(sessionId) {
|
|
|
56
56
|
return sessionId;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Phase 1.8: Log Normalization for CLI
|
|
61
|
+
* Intelligently transforms log data into hybrid JSON structure
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
function normalizeLevel(level) {
|
|
65
|
+
if (!level) return 'debug';
|
|
66
|
+
const levelStr = String(level).toLowerCase();
|
|
67
|
+
if (['debug', 'dbg', 'trace'].includes(levelStr)) return 'debug';
|
|
68
|
+
if (['info', 'information', 'log'].includes(levelStr)) return 'info';
|
|
69
|
+
if (['warn', 'warning', 'wrn'].includes(levelStr)) return 'warn';
|
|
70
|
+
if (['error', 'err', 'exception', 'fatal', 'critical'].includes(levelStr)) return 'error';
|
|
71
|
+
return 'debug';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractMetrics(payload) {
|
|
75
|
+
const metrics = {};
|
|
76
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
77
|
+
return metrics;
|
|
78
|
+
}
|
|
79
|
+
if (payload.metrics && typeof payload.metrics === 'object' && !Array.isArray(payload.metrics)) {
|
|
80
|
+
for (const [key, value] of Object.entries(payload.metrics)) {
|
|
81
|
+
if (typeof value === 'number') {
|
|
82
|
+
metrics[key] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return metrics;
|
|
86
|
+
}
|
|
87
|
+
const knownContextFields = new Set([
|
|
88
|
+
'trace_id', 'traceId', 'user_id', 'userId', 'request_id', 'requestId',
|
|
89
|
+
'correlation_id', 'correlationId', 'span_id', 'spanId', 'session_id', 'sessionId',
|
|
90
|
+
'id', 'pid', 'port', 'year', 'timestamp', 'time', 'date', 'createdAt', 'updatedAt',
|
|
91
|
+
'datetime', 'ts', 'utc', 'iso', 'exc_info', 'exception', 'error', 'message', 'msg', 'level', 'severity', 'log_level'
|
|
92
|
+
]);
|
|
93
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
94
|
+
const keyLower = key.toLowerCase();
|
|
95
|
+
if (knownContextFields.has(keyLower)) continue;
|
|
96
|
+
if (keyLower.includes('timestamp') || keyLower.includes('time') || keyLower.includes('date')) continue;
|
|
97
|
+
if (typeof value === 'number') {
|
|
98
|
+
if (key.endsWith('_ms') || key.endsWith('_count') || key.endsWith('_size') ||
|
|
99
|
+
key.endsWith('Ms') || key.endsWith('Count') || key.endsWith('Size') ||
|
|
100
|
+
['cpu', 'memory', 'latency', 'response_time', 'duration'].some(pattern => keyLower.includes(pattern))) {
|
|
101
|
+
metrics[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return metrics;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractContext(payload) {
|
|
109
|
+
const context = {};
|
|
110
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
111
|
+
return context;
|
|
112
|
+
}
|
|
113
|
+
if (payload.context && typeof payload.context === 'object' && !Array.isArray(payload.context)) {
|
|
114
|
+
return payload.context;
|
|
115
|
+
}
|
|
116
|
+
const knownContextFields = {
|
|
117
|
+
'trace_id': 'trace_id', 'traceId': 'trace_id',
|
|
118
|
+
'user_id': 'user_id', 'userId': 'user_id',
|
|
119
|
+
'request_id': 'request_id', 'requestId': 'request_id',
|
|
120
|
+
'correlation_id': 'correlation_id', 'correlationId': 'correlation_id',
|
|
121
|
+
'span_id': 'span_id', 'spanId': 'span_id',
|
|
122
|
+
'session_id': 'session_id', 'sessionId': 'session_id',
|
|
123
|
+
};
|
|
124
|
+
for (const [field, normalizedKey] of Object.entries(knownContextFields)) {
|
|
125
|
+
if (payload[field] !== undefined) {
|
|
126
|
+
context[normalizedKey] = payload[field];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return context;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeToHybrid(message, level, payload) {
|
|
133
|
+
const merged = { ...(payload || {}) };
|
|
134
|
+
let normalizedMessage = message;
|
|
135
|
+
if (!normalizedMessage && merged.message) normalizedMessage = merged.message;
|
|
136
|
+
if (!normalizedMessage && merged.msg) normalizedMessage = merged.msg;
|
|
137
|
+
const normalizedLevel = normalizeLevel(level || merged.level || merged.severity || merged.log_level);
|
|
138
|
+
const metrics = extractMetrics(merged);
|
|
139
|
+
const context = extractContext(merged);
|
|
140
|
+
const annotation = merged._annotation;
|
|
141
|
+
const hybrid = {
|
|
142
|
+
message: normalizedMessage, // Can be null/undefined
|
|
143
|
+
level: normalizedLevel,
|
|
144
|
+
metrics: metrics,
|
|
145
|
+
context: context,
|
|
146
|
+
};
|
|
147
|
+
if (annotation) hybrid._annotation = annotation;
|
|
148
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
149
|
+
if (!['message', 'msg', 'level', 'severity', 'log_level', 'metrics', 'context', '_annotation'].includes(key)) {
|
|
150
|
+
if (!(key in hybrid)) hybrid[key] = value;
|
|
151
|
+
}
|
|
76
152
|
}
|
|
153
|
+
return hybrid;
|
|
154
|
+
}
|
|
155
|
+
|
|
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://');
|
|
77
160
|
}
|
|
78
161
|
|
|
79
162
|
function getUrls(options) {
|
|
80
|
-
const {
|
|
163
|
+
const { web, socket, server } = options;
|
|
81
164
|
|
|
82
165
|
// Priority 1: Explicit --web and --socket flags (highest priority)
|
|
83
166
|
if (web) {
|
|
@@ -95,15 +178,7 @@ function getUrls(options) {
|
|
|
95
178
|
};
|
|
96
179
|
}
|
|
97
180
|
|
|
98
|
-
// Priority 3:
|
|
99
|
-
if (local) {
|
|
100
|
-
return {
|
|
101
|
-
webUrl: process.env.VIBEX_WEB_URL || 'http://localhost:3000',
|
|
102
|
-
socketUrl: process.env.VIBEX_SOCKET_URL || socket || 'ws://localhost:8787',
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Priority 4: Environment variables
|
|
181
|
+
// Priority 3: Environment variables
|
|
107
182
|
if (process.env.VIBEX_WEB_URL) {
|
|
108
183
|
return {
|
|
109
184
|
webUrl: process.env.VIBEX_WEB_URL,
|
|
@@ -111,8 +186,8 @@ function getUrls(options) {
|
|
|
111
186
|
};
|
|
112
187
|
}
|
|
113
188
|
|
|
114
|
-
// Priority
|
|
115
|
-
//
|
|
189
|
+
// Priority 4: Production defaults
|
|
190
|
+
// Always use production worker WebSocket endpoint
|
|
116
191
|
const defaultWorkerUrl = process.env.VIBEX_WORKER_URL || 'https://ingest.vibex.sh';
|
|
117
192
|
return {
|
|
118
193
|
webUrl: 'https://vibex.sh',
|
|
@@ -342,7 +417,6 @@ async function main() {
|
|
|
342
417
|
// Create a separate command instance for login
|
|
343
418
|
const loginCmd = new Command();
|
|
344
419
|
loginCmd
|
|
345
|
-
.option('-l, --local', 'Use localhost')
|
|
346
420
|
.option('--web <url>', 'Web server URL')
|
|
347
421
|
.option('--server <url>', 'Shorthand for --web');
|
|
348
422
|
|
|
@@ -362,9 +436,8 @@ async function main() {
|
|
|
362
436
|
program
|
|
363
437
|
.version(cliVersion, '-v, --version', 'Display version number')
|
|
364
438
|
.option('-s, --session-id <id>', 'Reuse existing session ID')
|
|
365
|
-
.option('
|
|
366
|
-
.option('--
|
|
367
|
-
.option('--socket <url>', 'Socket server URL (e.g., http://localhost:3001)')
|
|
439
|
+
.option('--web <url>', 'Web server URL')
|
|
440
|
+
.option('--socket <url>', 'Socket server URL')
|
|
368
441
|
.option('--server <url>', 'Shorthand for --web (auto-derives socket URL)')
|
|
369
442
|
.option('--token <token>', 'Authentication token (or use VIBEX_TOKEN env var)')
|
|
370
443
|
.parse();
|
|
@@ -419,10 +492,8 @@ async function main() {
|
|
|
419
492
|
|
|
420
493
|
// Print banner for new session
|
|
421
494
|
printBanner(sessionId, webUrl, authCode);
|
|
422
|
-
const localFlag = webUrl.includes('localhost') ? ' --local' : '';
|
|
423
|
-
const sessionSlug = sessionId.replace(/^vibex-/, ''); // Remove prefix for example
|
|
424
495
|
console.log(' 💡 Tip: Use -s to send more logs to this session');
|
|
425
|
-
console.log(` Example: echo '{"cpu": 45, "memory": 78}' | npx vibex-sh -s ${
|
|
496
|
+
console.log(` Example: echo '{"cpu": 45, "memory": 78}' | npx vibex-sh -s ${sessionId}\n`);
|
|
426
497
|
} catch (error) {
|
|
427
498
|
console.error(` ✗ Error creating session: ${error.message}`);
|
|
428
499
|
process.exit(1);
|
|
@@ -505,12 +576,27 @@ async function main() {
|
|
|
505
576
|
return;
|
|
506
577
|
}
|
|
507
578
|
|
|
579
|
+
// Clear any existing reconnect timeout to prevent duplicate connections
|
|
580
|
+
if (reconnectTimeout) {
|
|
581
|
+
clearTimeout(reconnectTimeout);
|
|
582
|
+
reconnectTimeout = null;
|
|
583
|
+
}
|
|
584
|
+
|
|
508
585
|
connectionLock = true;
|
|
509
586
|
connectionState = 'connecting';
|
|
510
587
|
connectionStartTime = Date.now();
|
|
511
588
|
console.log(' 🔄 Connecting to WebSocket...');
|
|
512
589
|
|
|
513
590
|
try {
|
|
591
|
+
// Close existing socket if any (cleanup)
|
|
592
|
+
if (socket && socket.readyState !== WebSocket.CLOSED) {
|
|
593
|
+
try {
|
|
594
|
+
socket.close();
|
|
595
|
+
} catch (e) {
|
|
596
|
+
// Ignore errors when closing
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
514
600
|
socket = new WebSocket(`${socketUrl}?sessionId=${sessionId}`);
|
|
515
601
|
|
|
516
602
|
socket.onopen = () => {
|
|
@@ -711,24 +797,14 @@ async function main() {
|
|
|
711
797
|
};
|
|
712
798
|
|
|
713
799
|
// Send logs via HTTP POST (non-blocking, same as SDKs)
|
|
714
|
-
//
|
|
800
|
+
// Always use production Cloudflare Worker endpoint
|
|
715
801
|
// Token is optional - anonymous sessions can send logs without authentication
|
|
716
802
|
// HTTP POST works independently of WebSocket - don't wait for WebSocket connection
|
|
717
803
|
const sendLogViaHTTP = async (logData) => {
|
|
718
804
|
try {
|
|
719
|
-
//
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
// Local development - use Workers dev server
|
|
723
|
-
ingestUrl = 'http://localhost:8787/api/v1/ingest';
|
|
724
|
-
} else if (process.env.VIBEX_WORKER_URL) {
|
|
725
|
-
// Use explicit Worker URL if set
|
|
726
|
-
ingestUrl = `${process.env.VIBEX_WORKER_URL}/api/v1/ingest`;
|
|
727
|
-
} else {
|
|
728
|
-
// Production default - use Worker URL (not web URL)
|
|
729
|
-
const defaultWorkerUrl = 'https://ingest.vibex.sh';
|
|
730
|
-
ingestUrl = `${defaultWorkerUrl}/api/v1/ingest`;
|
|
731
|
-
}
|
|
805
|
+
// Always use production worker URL
|
|
806
|
+
const workerUrl = process.env.VIBEX_WORKER_URL || 'https://ingest.vibex.sh';
|
|
807
|
+
const ingestUrl = `${workerUrl}/api/v1/ingest`;
|
|
732
808
|
|
|
733
809
|
// Build headers - only include Authorization if token exists
|
|
734
810
|
const headers = {
|
|
@@ -855,15 +931,37 @@ async function main() {
|
|
|
855
931
|
let logData;
|
|
856
932
|
try {
|
|
857
933
|
const parsed = JSON.parse(trimmedLine);
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
934
|
+
// Phase 1.8: Normalize JSON to hybrid structure
|
|
935
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
936
|
+
const hybrid = normalizeToHybrid(null, null, parsed);
|
|
937
|
+
logData = {
|
|
938
|
+
type: 'json',
|
|
939
|
+
payload: hybrid,
|
|
940
|
+
timestamp: Date.now(),
|
|
941
|
+
};
|
|
942
|
+
} else {
|
|
943
|
+
// Parsed but not an object - treat as text
|
|
944
|
+
logData = {
|
|
945
|
+
type: 'json',
|
|
946
|
+
payload: {
|
|
947
|
+
message: trimmedLine,
|
|
948
|
+
level: 'debug',
|
|
949
|
+
metrics: {},
|
|
950
|
+
context: {},
|
|
951
|
+
},
|
|
952
|
+
timestamp: Date.now(),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
863
955
|
} catch (e) {
|
|
956
|
+
// Phase 1.8: Text logs are now valuable - send as message field
|
|
864
957
|
logData = {
|
|
865
|
-
type: '
|
|
866
|
-
payload:
|
|
958
|
+
type: 'json',
|
|
959
|
+
payload: {
|
|
960
|
+
message: trimmedLine,
|
|
961
|
+
level: 'debug',
|
|
962
|
+
metrics: {},
|
|
963
|
+
context: {},
|
|
964
|
+
},
|
|
867
965
|
timestamp: Date.now(),
|
|
868
966
|
};
|
|
869
967
|
}
|