@dropout-ai/runtime 0.3.7 → 0.3.9

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 +74 -77
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.9",
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
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * @dropout-ai/runtime
3
3
  * Universal AI Interaction Capture for Node.js
4
- * Stability: High (Browser-Safe, Silent Failures)
4
+ * Feature: Always-On Heartbeat & Auto-Discovery
5
5
  */
6
6
 
7
- // We define these at the top scope but require them lazily or safely
8
- let https, http, crypto;
7
+ const https = require('https');
8
+ const http = require('http');
9
+ const crypto = require('crypto');
9
10
 
10
- // --- DEFAULT CONFIGURATION ---
11
+ // --- CONFIGURATION ---
11
12
  const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
12
13
 
13
14
  // Known AI Domains
@@ -18,73 +19,53 @@ const KNOWN_AI_DOMAINS = [
18
19
 
19
20
  class Dropout {
20
21
  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.
22
+ // đŸ›Ąī¸ BROWSER GUARD
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.");
26
25
  return;
27
26
  }
28
27
 
29
- // đŸ›Ąī¸ 2. SAFETY FUSE: MISSING CREDENTIALS
28
+ // đŸ›Ąī¸ CREDENTIAL GUARD
30
29
  if (!config.apiKey || !config.projectId) {
31
- if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: Missing API Key or Project ID.");
30
+ if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: Missing Credentials.");
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.
37
- try {
38
- https = require('https');
39
- http = require('http');
40
- crypto = require('crypto');
41
- } catch (e) {
42
- console.warn("[Dropout] âš ī¸ Skipped: Node.js core modules missing.");
43
- return;
34
+ // Initialize
35
+ this.config = config;
36
+ this.projectId = config.projectId;
37
+ this.apiKey = config.apiKey;
38
+ this.debug = config.debug || false;
39
+ this.privacy = config.privacy || 'full';
40
+ this.captureEndpoint = SUPABASE_FUNCTION_URL;
41
+ this.maxOutputBytes = 32768;
42
+
43
+ // State
44
+ this.turnIndex = 0;
45
+ this.lastTurnConfirmed = true;
46
+ this.lastPromptHash = null;
47
+ this.lastResponseHash = null;
48
+
49
+ // Singleton Guard
50
+ if (global.__dropout_initialized__) return;
51
+ global.__dropout_initialized__ = true;
52
+
53
+ if (!global.__dropout_session_id__) {
54
+ global.__dropout_session_id__ = this.generateSessionId();
44
55
  }
45
56
 
46
- // --- INITIALIZATION ---
47
- try {
48
- this.config = config;
49
- this.projectId = config.projectId;
50
- this.apiKey = config.apiKey;
51
- this.debug = config.debug || false;
52
- this.privacy = config.privacy || 'full';
53
- this.captureEndpoint = SUPABASE_FUNCTION_URL;
54
- this.maxOutputBytes = 32768;
55
-
56
- // Conversation State
57
- this.turnIndex = 0;
58
- this.lastTurnConfirmed = true;
59
- this.lastPromptHash = null;
60
- this.lastResponseHash = null;
61
-
62
- // Singleton Guard
63
- if (global.__dropout_initialized__) {
64
- if (this.debug) console.log("[Dropout] â„šī¸ Already initialized.");
65
- return;
66
- }
67
- global.__dropout_initialized__ = true;
57
+ if (this.debug) console.log(`[Dropout] đŸŸĸ Online: ${this.projectId}`);
68
58
 
69
- // Identity
70
- if (!global.__dropout_session_id__) {
71
- global.__dropout_session_id__ = this.generateSessionId();
72
- }
73
-
74
- if (this.debug) console.log(`[Dropout] đŸŸĸ Online: ${this.projectId}`);
59
+ // Bindings
60
+ this.log = this.log.bind(this);
61
+ this.emit = this.emit.bind(this);
75
62
 
76
- // Bindings
77
- this.log = this.log.bind(this);
78
- this.emit = this.emit.bind(this);
63
+ // 1. Start Network Interceptor
64
+ this.patchNetwork();
79
65
 
80
- // đŸ›Ąī¸ 4. SAFETY FUSE: NETWORK PATCHING
81
- // If patching fails, we log it and continue. We DO NOT crash the app.
82
- this.patchNetwork();
83
-
84
- } catch (err) {
85
- // THE ULTIMATE CATCH-ALL
86
- console.error("[Dropout] ❌ Critical Initialization Error (App will continue):", err);
87
- }
66
+ // 2. SEND STARTUP PING (Always run this, silent by default)
67
+ // This turns the dashboard status GREEN immediately on app boot.
68
+ this.sendStartupSignal();
88
69
  }
89
70
 
90
71
  // --- UTILS ---
@@ -100,6 +81,39 @@ class Dropout {
100
81
  }
101
82
  }
102
83
 
84
+ // --- HEARTBEAT ---
85
+ sendStartupSignal() {
86
+ // We send a discrete 'system_boot' event.
87
+ // The Database Trigger will see this and update projects.last_active_at
88
+ const payload = JSON.stringify({
89
+ project_id: this.projectId,
90
+ session_id: 'system_boot_' + Date.now(),
91
+ turn_role: 'system',
92
+ turn_index: 0,
93
+ content: 'Dropout SDK Initialized',
94
+ //content_blob: 'Dropout SDK Initialized',
95
+ metadata_flags: {
96
+ type: 'system_boot',
97
+ environment: process.env.NODE_ENV || 'development',
98
+ runtime: 'node'
99
+ },
100
+ received_at: new Date().toISOString()
101
+ });
102
+
103
+ const req = https.request(this.captureEndpoint, {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ 'x-dropout-key': this.apiKey
108
+ }
109
+ });
110
+
111
+ // Silent error handling (unless debug is on)
112
+ req.on('error', (e) => this.log("❌ Startup Signal Failed", e.message));
113
+ req.write(payload);
114
+ req.end();
115
+ }
116
+
103
117
  hash(text) {
104
118
  if (!text) return null;
105
119
  try {
@@ -121,22 +135,19 @@ class Dropout {
121
135
  else if (u.includes('cohere')) provider = 'cohere';
122
136
  else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
123
137
  }
124
-
125
138
  if (body) {
126
139
  const parsed = typeof body === 'string' ? JSON.parse(body) : body;
127
140
  if (parsed.model) model = parsed.model;
128
141
  if (provider === 'custom' && (parsed.messages || parsed.prompt)) provider = 'heuristic';
129
142
  }
130
- } catch (e) { /* Ignore parsing errors safely */ }
143
+ } catch (e) { }
131
144
  return { provider, model };
132
145
  }
133
146
 
134
147
  isAiRequest(url, bodyString) {
135
148
  if (!url) return false;
136
149
  if (url.includes(this.captureEndpoint)) return false;
137
-
138
150
  if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
139
-
140
151
  if (bodyString) {
141
152
  return (bodyString.includes('"model"') || bodyString.includes('"messages"')) &&
142
153
  (bodyString.includes('"user"') || bodyString.includes('"prompt"'));
@@ -166,7 +177,6 @@ class Dropout {
166
177
 
167
178
  this.log(`🚀 Sending Capture [${payload.turn_role}]`);
168
179
 
169
- // Use pure Node request to bypass patches
170
180
  const req = https.request(this.captureEndpoint, {
171
181
  method: 'POST',
172
182
  headers: {
@@ -191,17 +201,14 @@ class Dropout {
191
201
  if (global.fetch) {
192
202
  const originalFetch = global.fetch;
193
203
  global.fetch = async function (input, init) {
194
- // 1. Safe URL Extraction
195
204
  let url;
196
205
  try { url = typeof input === 'string' ? input : input?.url; } catch (e) { return originalFetch.apply(this, arguments); }
197
206
 
198
- // 2. Safe Body Extraction
199
207
  let bodyStr = "";
200
208
  if (init && init.body) {
201
209
  try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
202
210
  }
203
211
 
204
- // 3. Guard
205
212
  if (!_this.isAiRequest(url, bodyStr)) {
206
213
  return originalFetch.apply(this, arguments);
207
214
  }
@@ -209,7 +216,6 @@ class Dropout {
209
216
  _this.log(`⚡ [FETCH] Intercepting: ${url}`);
210
217
  const start = Date.now();
211
218
 
212
- // 4. Determine Turn
213
219
  let activeTurn;
214
220
  if (_this.lastTurnConfirmed) {
215
221
  activeTurn = _this.turnIndex++;
@@ -221,7 +227,6 @@ class Dropout {
221
227
  const { provider, model } = _this.normalize(url, bodyStr);
222
228
  const pHash = _this.hash(bodyStr);
223
229
 
224
- // 5. Emit User Request
225
230
  _this.emit({
226
231
  session_id: global.__dropout_session_id__,
227
232
  turn_index: activeTurn,
@@ -234,7 +239,6 @@ class Dropout {
234
239
  });
235
240
  _this.lastPromptHash = pHash;
236
241
 
237
- // 6. Execute Original (Wrapped)
238
242
  let response;
239
243
  try {
240
244
  response = await originalFetch.apply(this, arguments);
@@ -242,13 +246,10 @@ class Dropout {
242
246
 
243
247
  const latency = Date.now() - start;
244
248
 
245
- // 7. Emit AI Response (Non-Blocking)
246
249
  try {
247
250
  const cloned = response.clone();
248
251
  let oText = await cloned.text();
249
- if (oText && oText.length > _this.maxOutputBytes) {
250
- oText = oText.slice(0, _this.maxOutputBytes);
251
- }
252
+ if (oText && oText.length > _this.maxOutputBytes) oText = oText.slice(0, _this.maxOutputBytes);
252
253
  const oHash = _this.hash(oText);
253
254
 
254
255
  _this.emit({
@@ -304,7 +305,6 @@ class Dropout {
304
305
  const originalWrite = clientRequest.write;
305
306
  const originalEnd = clientRequest.end;
306
307
 
307
- // SAFE WRITE PATCH
308
308
  clientRequest.write = function (...writeArgs) {
309
309
  try {
310
310
  const chunk = writeArgs[0];
@@ -315,7 +315,6 @@ class Dropout {
315
315
  return originalWrite.apply(this, writeArgs);
316
316
  };
317
317
 
318
- // SAFE END PATCH
319
318
  clientRequest.end = function (...endArgs) {
320
319
  try {
321
320
  const chunk = endArgs[0];
@@ -355,7 +354,6 @@ class Dropout {
355
354
  return originalEnd.apply(this, endArgs);
356
355
  };
357
356
 
358
- // SAFE RESPONSE LISTENER
359
357
  clientRequest.on('response', (res) => {
360
358
  const resChunks = [];
361
359
  res.on('data', (chunk) => resChunks.push(chunk));
@@ -395,7 +393,6 @@ class Dropout {
395
393
  }
396
394
  }
397
395
 
398
- // Auto-Start (Server-Side Only)
399
396
  if (typeof process !== 'undefined' && process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
400
397
  new Dropout({
401
398
  projectId: process.env.DROPOUT_PROJECT_ID,