@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +52 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Invisible Node.js runtime for capturing AI interactions.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
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. SAFETY FUSE: BROWSER CHECK
22
- // If this runs in a browser (Client Component), ABORT IMMEDIATELY.
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: SDK detected browser environment. Running in Server-Mode only.");
24
+ if (config.debug) console.warn("[Dropout] ⚠️ Skipped: Browser detected. SDK is Server-Only.");
26
25
  return;
27
26
  }
28
27
 
29
- // 🛡️ 2. SAFETY FUSE: MISSING CREDENTIALS
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. SAFETY FUSE: DEPENDENCY CHECK
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.js core modules missing.");
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
- // Conversation State
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. SAFETY FUSE: NETWORK PATCHING
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] ❌ Critical Initialization Error (App will continue):", err);
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 safely */ }
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));