@dropout-ai/runtime 0.3.7 → 0.3.8
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/package.json +1 -1
- package/src/index.js +52 -34
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
3
|
* Universal AI Interaction Capture for Node.js
|
|
4
4
|
* Stability: High (Browser-Safe, Silent Failures)
|
|
5
|
+
* Features: Auto-Discovery, Connectivity Check, Dual-Schema Support
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
// We define these at the top scope but require them lazily or safely
|
|
8
8
|
let https, http, crypto;
|
|
9
9
|
|
|
10
10
|
// --- DEFAULT CONFIGURATION ---
|
|
@@ -18,28 +18,26 @@ const KNOWN_AI_DOMAINS = [
|
|
|
18
18
|
|
|
19
19
|
class Dropout {
|
|
20
20
|
constructor(config = {}) {
|
|
21
|
-
// 🛡️ 1.
|
|
22
|
-
// If this runs in a browser
|
|
23
|
-
// This prevents the "listener" error and protects your app from crashing.
|
|
21
|
+
// 🛡️ 1. BROWSER GUARD (Client-Side Protection)
|
|
22
|
+
// If this accidentally runs in a browser/Client Component, we abort silently.
|
|
24
23
|
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
|
|
25
|
-
if (config.debug) console.warn("[Dropout] ⚠️ Skipped:
|
|
24
|
+
if (config.debug) console.warn("[Dropout] ⚠️ Skipped: Browser detected. SDK is Server-Only.");
|
|
26
25
|
return;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
// 🛡️ 2.
|
|
28
|
+
// 🛡️ 2. CREDENTIAL CHECK
|
|
30
29
|
if (!config.apiKey || !config.projectId) {
|
|
31
30
|
if (config.debug) console.warn("[Dropout] ⚠️ Skipped: Missing API Key or Project ID.");
|
|
32
31
|
return;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
// 🛡️ 3.
|
|
36
|
-
// We try to load Node modules. If this fails (rare runtime edge case), we abort.
|
|
34
|
+
// 🛡️ 3. DEPENDENCY CHECK
|
|
37
35
|
try {
|
|
38
36
|
https = require('https');
|
|
39
37
|
http = require('http');
|
|
40
38
|
crypto = require('crypto');
|
|
41
39
|
} catch (e) {
|
|
42
|
-
console.warn("[Dropout] ⚠️ Skipped: Node
|
|
40
|
+
if (config.debug) console.warn("[Dropout] ⚠️ Skipped: Node core modules missing.");
|
|
43
41
|
return;
|
|
44
42
|
}
|
|
45
43
|
|
|
@@ -53,7 +51,7 @@ class Dropout {
|
|
|
53
51
|
this.captureEndpoint = SUPABASE_FUNCTION_URL;
|
|
54
52
|
this.maxOutputBytes = 32768;
|
|
55
53
|
|
|
56
|
-
//
|
|
54
|
+
// State
|
|
57
55
|
this.turnIndex = 0;
|
|
58
56
|
this.lastTurnConfirmed = true;
|
|
59
57
|
this.lastPromptHash = null;
|
|
@@ -71,19 +69,22 @@ class Dropout {
|
|
|
71
69
|
global.__dropout_session_id__ = this.generateSessionId();
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
if (this.debug) console.log(`[Dropout] 🟢 Online: ${this.projectId}`);
|
|
75
|
-
|
|
76
72
|
// Bindings
|
|
77
73
|
this.log = this.log.bind(this);
|
|
78
74
|
this.emit = this.emit.bind(this);
|
|
79
75
|
|
|
80
|
-
// 🛡️ 4.
|
|
81
|
-
// If patching fails, we log it and continue. We DO NOT crash the app.
|
|
76
|
+
// 🛡️ 4. SAFE STARTUP
|
|
82
77
|
this.patchNetwork();
|
|
83
78
|
|
|
79
|
+
// 🔍 5. CONNECTION VERIFICATION (Async Ping)
|
|
80
|
+
// Only runs if debug is TRUE to confirm setup
|
|
81
|
+
if (this.debug) {
|
|
82
|
+
this.validateConnection();
|
|
83
|
+
}
|
|
84
|
+
|
|
84
85
|
} catch (err) {
|
|
85
|
-
// THE ULTIMATE CATCH-ALL
|
|
86
|
-
console.error("[Dropout] ❌
|
|
86
|
+
// THE ULTIMATE CATCH-ALL: Ensure app NEVER crashes
|
|
87
|
+
if (config.debug) console.error("[Dropout] ❌ Initialization Error:", err);
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -100,6 +101,39 @@ class Dropout {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
// --- HEALTH CHECK ---
|
|
105
|
+
validateConnection() {
|
|
106
|
+
this.log("🔄 Verifying connection to Dropout Cloud...");
|
|
107
|
+
|
|
108
|
+
const req = https.request(this.captureEndpoint, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
'x-dropout-key': this.apiKey
|
|
113
|
+
}
|
|
114
|
+
}, (res) => {
|
|
115
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
116
|
+
console.log(`[Dropout] ✅ Connected to Project: ${this.projectId}`);
|
|
117
|
+
} else {
|
|
118
|
+
console.warn(`[Dropout] ⚠️ Connection Failed (Status: ${res.statusCode}). Check your API Key.`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
req.on('error', (e) => console.warn(`[Dropout] ❌ Connection Error: ${e.message}`));
|
|
123
|
+
|
|
124
|
+
// Send lightweight Ping payload
|
|
125
|
+
req.write(JSON.stringify({
|
|
126
|
+
project_id: this.projectId,
|
|
127
|
+
session_id: 'system_ping',
|
|
128
|
+
turn_role: 'system',
|
|
129
|
+
turn_index: 0,
|
|
130
|
+
content: 'ping',
|
|
131
|
+
//content_blob: 'ping',
|
|
132
|
+
metadata_flags: { type: 'connectivity_check' }
|
|
133
|
+
}));
|
|
134
|
+
req.end();
|
|
135
|
+
}
|
|
136
|
+
|
|
103
137
|
hash(text) {
|
|
104
138
|
if (!text) return null;
|
|
105
139
|
try {
|
|
@@ -121,22 +155,19 @@ class Dropout {
|
|
|
121
155
|
else if (u.includes('cohere')) provider = 'cohere';
|
|
122
156
|
else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
|
|
123
157
|
}
|
|
124
|
-
|
|
125
158
|
if (body) {
|
|
126
159
|
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
|
|
127
160
|
if (parsed.model) model = parsed.model;
|
|
128
161
|
if (provider === 'custom' && (parsed.messages || parsed.prompt)) provider = 'heuristic';
|
|
129
162
|
}
|
|
130
|
-
} catch (e) { /* Ignore parsing errors
|
|
163
|
+
} catch (e) { /* Ignore parsing errors */ }
|
|
131
164
|
return { provider, model };
|
|
132
165
|
}
|
|
133
166
|
|
|
134
167
|
isAiRequest(url, bodyString) {
|
|
135
168
|
if (!url) return false;
|
|
136
169
|
if (url.includes(this.captureEndpoint)) return false;
|
|
137
|
-
|
|
138
170
|
if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
|
|
139
|
-
|
|
140
171
|
if (bodyString) {
|
|
141
172
|
return (bodyString.includes('"model"') || bodyString.includes('"messages"')) &&
|
|
142
173
|
(bodyString.includes('"user"') || bodyString.includes('"prompt"'));
|
|
@@ -166,7 +197,6 @@ class Dropout {
|
|
|
166
197
|
|
|
167
198
|
this.log(`🚀 Sending Capture [${payload.turn_role}]`);
|
|
168
199
|
|
|
169
|
-
// Use pure Node request to bypass patches
|
|
170
200
|
const req = https.request(this.captureEndpoint, {
|
|
171
201
|
method: 'POST',
|
|
172
202
|
headers: {
|
|
@@ -191,17 +221,14 @@ class Dropout {
|
|
|
191
221
|
if (global.fetch) {
|
|
192
222
|
const originalFetch = global.fetch;
|
|
193
223
|
global.fetch = async function (input, init) {
|
|
194
|
-
// 1. Safe URL Extraction
|
|
195
224
|
let url;
|
|
196
225
|
try { url = typeof input === 'string' ? input : input?.url; } catch (e) { return originalFetch.apply(this, arguments); }
|
|
197
226
|
|
|
198
|
-
// 2. Safe Body Extraction
|
|
199
227
|
let bodyStr = "";
|
|
200
228
|
if (init && init.body) {
|
|
201
229
|
try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
202
230
|
}
|
|
203
231
|
|
|
204
|
-
// 3. Guard
|
|
205
232
|
if (!_this.isAiRequest(url, bodyStr)) {
|
|
206
233
|
return originalFetch.apply(this, arguments);
|
|
207
234
|
}
|
|
@@ -209,7 +236,6 @@ class Dropout {
|
|
|
209
236
|
_this.log(`⚡ [FETCH] Intercepting: ${url}`);
|
|
210
237
|
const start = Date.now();
|
|
211
238
|
|
|
212
|
-
// 4. Determine Turn
|
|
213
239
|
let activeTurn;
|
|
214
240
|
if (_this.lastTurnConfirmed) {
|
|
215
241
|
activeTurn = _this.turnIndex++;
|
|
@@ -221,7 +247,6 @@ class Dropout {
|
|
|
221
247
|
const { provider, model } = _this.normalize(url, bodyStr);
|
|
222
248
|
const pHash = _this.hash(bodyStr);
|
|
223
249
|
|
|
224
|
-
// 5. Emit User Request
|
|
225
250
|
_this.emit({
|
|
226
251
|
session_id: global.__dropout_session_id__,
|
|
227
252
|
turn_index: activeTurn,
|
|
@@ -234,7 +259,6 @@ class Dropout {
|
|
|
234
259
|
});
|
|
235
260
|
_this.lastPromptHash = pHash;
|
|
236
261
|
|
|
237
|
-
// 6. Execute Original (Wrapped)
|
|
238
262
|
let response;
|
|
239
263
|
try {
|
|
240
264
|
response = await originalFetch.apply(this, arguments);
|
|
@@ -242,13 +266,10 @@ class Dropout {
|
|
|
242
266
|
|
|
243
267
|
const latency = Date.now() - start;
|
|
244
268
|
|
|
245
|
-
// 7. Emit AI Response (Non-Blocking)
|
|
246
269
|
try {
|
|
247
270
|
const cloned = response.clone();
|
|
248
271
|
let oText = await cloned.text();
|
|
249
|
-
if (oText && oText.length > _this.maxOutputBytes)
|
|
250
|
-
oText = oText.slice(0, _this.maxOutputBytes);
|
|
251
|
-
}
|
|
272
|
+
if (oText && oText.length > _this.maxOutputBytes) oText = oText.slice(0, _this.maxOutputBytes);
|
|
252
273
|
const oHash = _this.hash(oText);
|
|
253
274
|
|
|
254
275
|
_this.emit({
|
|
@@ -304,7 +325,6 @@ class Dropout {
|
|
|
304
325
|
const originalWrite = clientRequest.write;
|
|
305
326
|
const originalEnd = clientRequest.end;
|
|
306
327
|
|
|
307
|
-
// SAFE WRITE PATCH
|
|
308
328
|
clientRequest.write = function (...writeArgs) {
|
|
309
329
|
try {
|
|
310
330
|
const chunk = writeArgs[0];
|
|
@@ -315,7 +335,6 @@ class Dropout {
|
|
|
315
335
|
return originalWrite.apply(this, writeArgs);
|
|
316
336
|
};
|
|
317
337
|
|
|
318
|
-
// SAFE END PATCH
|
|
319
338
|
clientRequest.end = function (...endArgs) {
|
|
320
339
|
try {
|
|
321
340
|
const chunk = endArgs[0];
|
|
@@ -355,7 +374,6 @@ class Dropout {
|
|
|
355
374
|
return originalEnd.apply(this, endArgs);
|
|
356
375
|
};
|
|
357
376
|
|
|
358
|
-
// SAFE RESPONSE LISTENER
|
|
359
377
|
clientRequest.on('response', (res) => {
|
|
360
378
|
const resChunks = [];
|
|
361
379
|
res.on('data', (chunk) => resChunks.push(chunk));
|