@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.
- package/package.json +1 -1
- package/src/index.js +74 -77
package/package.json
CHANGED
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
|
-
*
|
|
4
|
+
* Feature: Always-On Heartbeat & Auto-Discovery
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const crypto = require('crypto');
|
|
9
10
|
|
|
10
|
-
// ---
|
|
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
|
-
// đĄī¸
|
|
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:
|
|
24
|
+
if (config.debug) console.warn("[Dropout] â ī¸ Skipped: Browser detected.");
|
|
26
25
|
return;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
// đĄī¸
|
|
28
|
+
// đĄī¸ CREDENTIAL GUARD
|
|
30
29
|
if (!config.apiKey || !config.projectId) {
|
|
31
|
-
if (config.debug) console.warn("[Dropout] â ī¸ Skipped: Missing
|
|
30
|
+
if (config.debug) console.warn("[Dropout] â ī¸ Skipped: Missing Credentials.");
|
|
32
31
|
return;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
this.emit = this.emit.bind(this);
|
|
63
|
+
// 1. Start Network Interceptor
|
|
64
|
+
this.patchNetwork();
|
|
79
65
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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) {
|
|
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,
|