@astermind/cybernetic-chatbot-client 2.2.36 → 2.2.48
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 +102 -5
- package/dist/ApiClient.d.ts +4 -3
- package/dist/ApiClient.d.ts.map +1 -1
- package/dist/CyberneticClient.d.ts +11 -0
- package/dist/CyberneticClient.d.ts.map +1 -1
- package/dist/WebSocketTransport.d.ts +104 -0
- package/dist/WebSocketTransport.d.ts.map +1 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/cybernetic-chatbot-client-full.esm.js +713 -188
- package/dist/cybernetic-chatbot-client-full.esm.js.map +1 -1
- package/dist/cybernetic-chatbot-client-full.min.js +1 -1
- package/dist/cybernetic-chatbot-client-full.min.js.map +1 -1
- package/dist/cybernetic-chatbot-client-full.umd.js +713 -188
- package/dist/cybernetic-chatbot-client-full.umd.js.map +1 -1
- package/dist/cybernetic-chatbot-client.esm.js +740 -215
- package/dist/cybernetic-chatbot-client.esm.js.map +1 -1
- package/dist/cybernetic-chatbot-client.min.js +1 -1
- package/dist/cybernetic-chatbot-client.min.js.map +1 -1
- package/dist/cybernetic-chatbot-client.umd.js +742 -214
- package/dist/cybernetic-chatbot-client.umd.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +36 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
// src/ApiClient.ts
|
|
8
8
|
// HTTP client for AsterMind backend API
|
|
9
9
|
/**
|
|
10
|
-
* Map backend source format to client Source interface
|
|
10
|
+
* Map backend source format to client Source interface.
|
|
11
|
+
* Handles both canonical (title/score) and legacy (heading/no-score) field names.
|
|
11
12
|
*/
|
|
12
13
|
function mapSource(raw) {
|
|
13
14
|
return {
|
|
14
|
-
title: raw.title,
|
|
15
|
-
snippet: raw.content,
|
|
16
|
-
relevance: raw.score,
|
|
15
|
+
title: raw.title || raw.heading || 'Document',
|
|
16
|
+
snippet: raw.content || '',
|
|
17
|
+
relevance: raw.score ?? 0,
|
|
17
18
|
documentId: raw.documentId,
|
|
18
19
|
fullContent: raw.fullContent,
|
|
19
20
|
downloadUrl: raw.downloadUrl,
|
|
@@ -51,9 +52,10 @@
|
|
|
51
52
|
throw new Error(error.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
52
53
|
}
|
|
53
54
|
const data = await response.json();
|
|
54
|
-
//
|
|
55
|
+
// Normalize response: accept both 'reply' (canonical) and 'answer' (legacy)
|
|
55
56
|
return {
|
|
56
57
|
...data,
|
|
58
|
+
reply: data.reply || data.answer || '',
|
|
57
59
|
sources: (data.sources || []).map(mapSource)
|
|
58
60
|
};
|
|
59
61
|
}
|
|
@@ -129,7 +131,8 @@
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
/**
|
|
132
|
-
* Get general documents for caching
|
|
134
|
+
* Get general documents for caching.
|
|
135
|
+
* Normalizes field names from both canonical (title) and legacy (name) formats.
|
|
133
136
|
*/
|
|
134
137
|
async getGeneralDocs(since) {
|
|
135
138
|
const params = new URLSearchParams();
|
|
@@ -146,10 +149,16 @@
|
|
|
146
149
|
throw new Error(`HTTP ${response.status}`);
|
|
147
150
|
}
|
|
148
151
|
const data = await response.json();
|
|
149
|
-
|
|
152
|
+
// Normalize: accept both 'title' (canonical) and 'name' (legacy on-prem)
|
|
153
|
+
return (data.documents || []).map((doc) => ({
|
|
154
|
+
id: doc.id,
|
|
155
|
+
title: doc.title || doc.name || 'Untitled',
|
|
156
|
+
updatedAt: doc.updatedAt || doc.updated_at || new Date().toISOString(),
|
|
157
|
+
}));
|
|
150
158
|
}
|
|
151
159
|
/**
|
|
152
|
-
* Get API status, quota, and system settings
|
|
160
|
+
* Get API status, quota, and system settings.
|
|
161
|
+
* Normalizes response from both canonical and legacy backend formats.
|
|
153
162
|
*/
|
|
154
163
|
async getStatus() {
|
|
155
164
|
const response = await fetch(`${this.baseUrl}/api/external/status`, {
|
|
@@ -160,7 +169,38 @@
|
|
|
160
169
|
if (!response.ok) {
|
|
161
170
|
throw new Error(`HTTP ${response.status}`);
|
|
162
171
|
}
|
|
163
|
-
|
|
172
|
+
const data = await response.json();
|
|
173
|
+
// Normalize: accept both canonical (apiKey) and legacy (key) structures
|
|
174
|
+
const apiKeyData = data.apiKey || data.key || {};
|
|
175
|
+
const scopes = apiKeyData.scopes || apiKeyData.permissions || ['chat'];
|
|
176
|
+
// Normalize quota: accept both canonical (perMinute/perDay) and legacy structures
|
|
177
|
+
const quota = data.quota || {};
|
|
178
|
+
const perMinute = quota.perMinute || {
|
|
179
|
+
limit: apiKeyData.rateLimit?.limit || quota.rateLimitPerMinute || 30,
|
|
180
|
+
remaining: apiKeyData.rateLimit?.remaining ?? quota.rateLimitPerMinute ?? 30,
|
|
181
|
+
};
|
|
182
|
+
const perDay = quota.perDay || {
|
|
183
|
+
limit: quota.rateLimitPerDay || perMinute.limit * 60 * 24,
|
|
184
|
+
remaining: quota.rateLimitPerDay || perMinute.limit * 60 * 24,
|
|
185
|
+
};
|
|
186
|
+
// Normalize systemSettings: accept both canonical and legacy (system) structures
|
|
187
|
+
const settings = data.systemSettings || data.system || {};
|
|
188
|
+
const cacheRetentionHours = settings.cacheRetentionHours ??
|
|
189
|
+
(settings.cacheRetentionDays ? settings.cacheRetentionDays * 24 : undefined);
|
|
190
|
+
return {
|
|
191
|
+
status: data.status || 'active',
|
|
192
|
+
apiKey: {
|
|
193
|
+
id: apiKeyData.id || '',
|
|
194
|
+
scopes,
|
|
195
|
+
},
|
|
196
|
+
quota: { perMinute, perDay },
|
|
197
|
+
systemSettings: {
|
|
198
|
+
cacheRetentionHours: cacheRetentionHours ?? 720,
|
|
199
|
+
maintenanceMode: settings.maintenanceMode ?? false,
|
|
200
|
+
maintenanceMessage: settings.maintenanceMessage,
|
|
201
|
+
forceOfflineClients: settings.forceOfflineClients ?? false,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
164
204
|
}
|
|
165
205
|
/**
|
|
166
206
|
* Health check (no auth required)
|
|
@@ -995,26 +1035,624 @@
|
|
|
995
1035
|
await db.clear('vectors');
|
|
996
1036
|
console.log('[CyberneticOfflineStorage] Cache cleared');
|
|
997
1037
|
}
|
|
998
|
-
/**
|
|
999
|
-
* Get the database connection
|
|
1000
|
-
*/
|
|
1001
|
-
async getDB() {
|
|
1002
|
-
if (this.db) {
|
|
1003
|
-
return this.db;
|
|
1004
|
-
}
|
|
1005
|
-
return this.initDB();
|
|
1038
|
+
/**
|
|
1039
|
+
* Get the database connection
|
|
1040
|
+
*/
|
|
1041
|
+
async getDB() {
|
|
1042
|
+
if (this.db) {
|
|
1043
|
+
return this.db;
|
|
1044
|
+
}
|
|
1045
|
+
return this.initDB();
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Close database connection
|
|
1049
|
+
*/
|
|
1050
|
+
close() {
|
|
1051
|
+
if (this.db) {
|
|
1052
|
+
this.db.close();
|
|
1053
|
+
this.db = null;
|
|
1054
|
+
this.dbPromise = null;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/WebSocketTransport.ts
|
|
1060
|
+
// WebSocket transport for streaming chat via SaaS WebSocket API (chatws.astermind.ai)
|
|
1061
|
+
/**
|
|
1062
|
+
* WebSocket transport for streaming chat.
|
|
1063
|
+
*
|
|
1064
|
+
* Maps the chat-handler Lambda WebSocket protocol to the client's StreamCallbacks:
|
|
1065
|
+
* Server typing → ignored (UI has its own indicator)
|
|
1066
|
+
* Server chunk → onToken(content)
|
|
1067
|
+
* Server done → onSources(sources) + onComplete(response)
|
|
1068
|
+
* Server error → onError(error)
|
|
1069
|
+
*
|
|
1070
|
+
* Connection is lazy — established on the first chatStream() call.
|
|
1071
|
+
* Reuses the same connection across multiple messages.
|
|
1072
|
+
* Reconnects with exponential backoff on close/error.
|
|
1073
|
+
*/
|
|
1074
|
+
class WebSocketTransport {
|
|
1075
|
+
constructor(wsUrl, apiKey, options) {
|
|
1076
|
+
this.ws = null;
|
|
1077
|
+
this.state = 'disconnected';
|
|
1078
|
+
this.reconnectAttempts = 0;
|
|
1079
|
+
this.connectionPromise = null;
|
|
1080
|
+
// Active streaming request tracking
|
|
1081
|
+
this.activeCallbacks = null;
|
|
1082
|
+
this.activeFullText = '';
|
|
1083
|
+
// Cleanup handler reference
|
|
1084
|
+
this.beforeUnloadHandler = null;
|
|
1085
|
+
// Promise callbacks for the active stream
|
|
1086
|
+
this._resolveStream = null;
|
|
1087
|
+
this._rejectStream = null;
|
|
1088
|
+
this.wsUrl = wsUrl;
|
|
1089
|
+
this.apiKey = apiKey;
|
|
1090
|
+
this.maxReconnectAttempts = options?.maxReconnectAttempts ?? 3;
|
|
1091
|
+
this.reconnectDelay = options?.reconnectDelay ?? 1000;
|
|
1092
|
+
this.connectionTimeout = options?.connectionTimeout ?? 10000;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Current transport state
|
|
1096
|
+
*/
|
|
1097
|
+
getState() {
|
|
1098
|
+
return this.state;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Whether the WebSocket is currently connected
|
|
1102
|
+
*/
|
|
1103
|
+
isConnected() {
|
|
1104
|
+
return this.state === 'connected' && this.ws?.readyState === WebSocket.OPEN;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Connect to the WebSocket endpoint.
|
|
1108
|
+
* Appends the API key as a query parameter for authentication.
|
|
1109
|
+
* Returns a promise that resolves when the connection is open.
|
|
1110
|
+
*/
|
|
1111
|
+
connect() {
|
|
1112
|
+
// Already connected
|
|
1113
|
+
if (this.isConnected()) {
|
|
1114
|
+
return Promise.resolve();
|
|
1115
|
+
}
|
|
1116
|
+
// Connection already in progress
|
|
1117
|
+
if (this.connectionPromise) {
|
|
1118
|
+
return this.connectionPromise;
|
|
1119
|
+
}
|
|
1120
|
+
this.state = 'connecting';
|
|
1121
|
+
this.connectionPromise = new Promise((resolve, reject) => {
|
|
1122
|
+
try {
|
|
1123
|
+
// Build URL with API key
|
|
1124
|
+
const separator = this.wsUrl.includes('?') ? '&' : '?';
|
|
1125
|
+
const fullUrl = `${this.wsUrl}${separator}apiKey=${encodeURIComponent(this.apiKey)}`;
|
|
1126
|
+
const ws = new WebSocket(fullUrl);
|
|
1127
|
+
this.ws = ws;
|
|
1128
|
+
// Connection timeout
|
|
1129
|
+
const timeout = setTimeout(() => {
|
|
1130
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1131
|
+
ws.close();
|
|
1132
|
+
this.state = 'error';
|
|
1133
|
+
this.connectionPromise = null;
|
|
1134
|
+
reject(new Error('WebSocket connection timed out'));
|
|
1135
|
+
}
|
|
1136
|
+
}, this.connectionTimeout);
|
|
1137
|
+
ws.onopen = () => {
|
|
1138
|
+
clearTimeout(timeout);
|
|
1139
|
+
this.state = 'connected';
|
|
1140
|
+
this.reconnectAttempts = 0;
|
|
1141
|
+
this.connectionPromise = null;
|
|
1142
|
+
this.registerCleanup();
|
|
1143
|
+
resolve();
|
|
1144
|
+
};
|
|
1145
|
+
ws.onmessage = (event) => {
|
|
1146
|
+
this.handleMessage(event);
|
|
1147
|
+
};
|
|
1148
|
+
ws.onerror = () => {
|
|
1149
|
+
clearTimeout(timeout);
|
|
1150
|
+
this.state = 'error';
|
|
1151
|
+
this.connectionPromise = null;
|
|
1152
|
+
// The close event fires after error, which will trigger reconnect
|
|
1153
|
+
};
|
|
1154
|
+
ws.onclose = (event) => {
|
|
1155
|
+
clearTimeout(timeout);
|
|
1156
|
+
const wasConnected = this.state === 'connected';
|
|
1157
|
+
this.state = 'disconnected';
|
|
1158
|
+
this.connectionPromise = null;
|
|
1159
|
+
this.unregisterCleanup();
|
|
1160
|
+
// If we had an active request, report the error
|
|
1161
|
+
if (this.activeCallbacks) {
|
|
1162
|
+
this.activeCallbacks.onError?.({
|
|
1163
|
+
code: 'WS_ERROR',
|
|
1164
|
+
message: event.reason || `WebSocket closed (code: ${event.code})`
|
|
1165
|
+
});
|
|
1166
|
+
this.clearActiveRequest();
|
|
1167
|
+
}
|
|
1168
|
+
// Reject if we were trying to connect
|
|
1169
|
+
if (!wasConnected) {
|
|
1170
|
+
reject(new Error(event.reason || `WebSocket closed (code: ${event.code})`));
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
catch (error) {
|
|
1175
|
+
this.state = 'error';
|
|
1176
|
+
this.connectionPromise = null;
|
|
1177
|
+
reject(error);
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
return this.connectionPromise;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Stream a chat message over WebSocket.
|
|
1184
|
+
*
|
|
1185
|
+
* Sends: { type: "sendMessage", message, sessionId?, wordLimit?, context? }
|
|
1186
|
+
* Receives: typing → chunk × N → done (with sources and metadata)
|
|
1187
|
+
*/
|
|
1188
|
+
async chatStream(message, options) {
|
|
1189
|
+
const { sessionId, context, wordLimit, onToken, onSources, onComplete, onError } = options;
|
|
1190
|
+
// Ensure connection is established (lazy connect)
|
|
1191
|
+
try {
|
|
1192
|
+
await this.connect();
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
onError?.({
|
|
1196
|
+
code: 'WS_ERROR',
|
|
1197
|
+
message: `Failed to connect: ${error instanceof Error ? error.message : String(error)}`
|
|
1198
|
+
});
|
|
1199
|
+
throw error;
|
|
1200
|
+
}
|
|
1201
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1202
|
+
const err = { code: 'WS_ERROR', message: 'WebSocket not connected' };
|
|
1203
|
+
onError?.(err);
|
|
1204
|
+
throw new Error(err.message);
|
|
1205
|
+
}
|
|
1206
|
+
// Set up active request tracking
|
|
1207
|
+
this.activeCallbacks = { onToken, onSources, onComplete, onError };
|
|
1208
|
+
this.activeFullText = '';
|
|
1209
|
+
this.activeSessionId = sessionId;
|
|
1210
|
+
// Build and send the message
|
|
1211
|
+
const payload = {
|
|
1212
|
+
type: 'sendMessage',
|
|
1213
|
+
message,
|
|
1214
|
+
};
|
|
1215
|
+
if (sessionId)
|
|
1216
|
+
payload.sessionId = sessionId;
|
|
1217
|
+
if (wordLimit)
|
|
1218
|
+
payload.wordLimit = wordLimit;
|
|
1219
|
+
if (context)
|
|
1220
|
+
payload.context = context;
|
|
1221
|
+
// Return a promise that resolves when done/error is received
|
|
1222
|
+
return new Promise((resolve, reject) => {
|
|
1223
|
+
// Store resolve/reject so handleMessage can call them
|
|
1224
|
+
this._resolveStream = resolve;
|
|
1225
|
+
this._rejectStream = reject;
|
|
1226
|
+
this.ws.send(JSON.stringify(payload));
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Disconnect and clean up the WebSocket connection
|
|
1231
|
+
*/
|
|
1232
|
+
disconnect() {
|
|
1233
|
+
this.unregisterCleanup();
|
|
1234
|
+
if (this.ws) {
|
|
1235
|
+
// Remove handlers to avoid triggering reconnect
|
|
1236
|
+
this.ws.onclose = null;
|
|
1237
|
+
this.ws.onerror = null;
|
|
1238
|
+
this.ws.onmessage = null;
|
|
1239
|
+
this.ws.close();
|
|
1240
|
+
this.ws = null;
|
|
1241
|
+
}
|
|
1242
|
+
this.state = 'disconnected';
|
|
1243
|
+
this.connectionPromise = null;
|
|
1244
|
+
this.clearActiveRequest();
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Attempt reconnection with exponential backoff.
|
|
1248
|
+
* Used internally after connection loss.
|
|
1249
|
+
*/
|
|
1250
|
+
async reconnect() {
|
|
1251
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1252
|
+
this.state = 'error';
|
|
1253
|
+
throw new Error(`Max reconnection attempts (${this.maxReconnectAttempts}) exceeded`);
|
|
1254
|
+
}
|
|
1255
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
|
|
1256
|
+
this.reconnectAttempts++;
|
|
1257
|
+
console.log(`[Cybernetic WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
1258
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1259
|
+
// Ensure old connection is cleaned up
|
|
1260
|
+
if (this.ws) {
|
|
1261
|
+
this.ws.onclose = null;
|
|
1262
|
+
this.ws.onerror = null;
|
|
1263
|
+
this.ws.close();
|
|
1264
|
+
this.ws = null;
|
|
1265
|
+
}
|
|
1266
|
+
return this.connect();
|
|
1267
|
+
}
|
|
1268
|
+
// ==================== INTERNAL ====================
|
|
1269
|
+
/**
|
|
1270
|
+
* Handle incoming WebSocket messages from the chat-handler Lambda.
|
|
1271
|
+
*
|
|
1272
|
+
* Protocol:
|
|
1273
|
+
* { type: 'typing', status: boolean } → ignored
|
|
1274
|
+
* { type: 'chunk', content: string } → onToken(content)
|
|
1275
|
+
* { type: 'done', sessionId, sources, metadata, messageId } → onSources + onComplete
|
|
1276
|
+
* { type: 'error', error: string } → onError
|
|
1277
|
+
*/
|
|
1278
|
+
handleMessage(event) {
|
|
1279
|
+
if (!this.activeCallbacks)
|
|
1280
|
+
return;
|
|
1281
|
+
try {
|
|
1282
|
+
const data = JSON.parse(event.data);
|
|
1283
|
+
switch (data.type) {
|
|
1284
|
+
case 'typing':
|
|
1285
|
+
// Ignored — the chatbot-template manages its own typing indicator
|
|
1286
|
+
break;
|
|
1287
|
+
case 'chunk':
|
|
1288
|
+
if (data.content) {
|
|
1289
|
+
this.activeFullText += data.content;
|
|
1290
|
+
this.activeCallbacks.onToken?.(data.content);
|
|
1291
|
+
}
|
|
1292
|
+
break;
|
|
1293
|
+
case 'done': {
|
|
1294
|
+
// Map sources from chat-handler format to client Source format
|
|
1295
|
+
const sources = this.mapSources(data.sources || []);
|
|
1296
|
+
// Emit sources
|
|
1297
|
+
this.activeCallbacks.onSources?.(sources);
|
|
1298
|
+
// Build complete response
|
|
1299
|
+
const response = {
|
|
1300
|
+
reply: this.activeFullText,
|
|
1301
|
+
confidence: 'high',
|
|
1302
|
+
sources,
|
|
1303
|
+
offline: false,
|
|
1304
|
+
sessionId: data.sessionId || this.activeSessionId,
|
|
1305
|
+
};
|
|
1306
|
+
this.activeCallbacks.onComplete?.(response);
|
|
1307
|
+
this._resolveStream?.();
|
|
1308
|
+
this.clearActiveRequest();
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
case 'error': {
|
|
1312
|
+
const err = {
|
|
1313
|
+
code: 'WS_ERROR',
|
|
1314
|
+
message: data.error || 'Unknown WebSocket error'
|
|
1315
|
+
};
|
|
1316
|
+
this.activeCallbacks.onError?.(err);
|
|
1317
|
+
this._rejectStream?.(new Error(err.message));
|
|
1318
|
+
this.clearActiveRequest();
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
default:
|
|
1322
|
+
// Unknown message type — log and ignore
|
|
1323
|
+
console.warn('[Cybernetic WS] Unknown message type:', data.type);
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
catch (error) {
|
|
1328
|
+
console.error('[Cybernetic WS] Failed to parse message:', error, event.data);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Map sources from the chat-handler WebSocket format to the client Source interface.
|
|
1333
|
+
*
|
|
1334
|
+
* Chat-handler sends: { heading, content, score, documentId, documentName }
|
|
1335
|
+
* Client expects: { title, snippet, relevance, documentId, documentName }
|
|
1336
|
+
*/
|
|
1337
|
+
mapSources(wsSources) {
|
|
1338
|
+
if (!Array.isArray(wsSources))
|
|
1339
|
+
return [];
|
|
1340
|
+
return wsSources.map((s) => ({
|
|
1341
|
+
title: s.title || s.heading || s.documentName || 'Document',
|
|
1342
|
+
snippet: s.content || '',
|
|
1343
|
+
relevance: s.score ?? 0,
|
|
1344
|
+
documentId: s.documentId,
|
|
1345
|
+
documentName: s.documentName,
|
|
1346
|
+
sourceType: s.sourceType || 'document',
|
|
1347
|
+
chunkId: s.chunkId,
|
|
1348
|
+
}));
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Clear active request state after completion or error
|
|
1352
|
+
*/
|
|
1353
|
+
clearActiveRequest() {
|
|
1354
|
+
this.activeCallbacks = null;
|
|
1355
|
+
this.activeFullText = '';
|
|
1356
|
+
this._resolveStream = null;
|
|
1357
|
+
this._rejectStream = null;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Register a beforeunload handler to close the WebSocket on page unload
|
|
1361
|
+
*/
|
|
1362
|
+
registerCleanup() {
|
|
1363
|
+
if (typeof window !== 'undefined' && !this.beforeUnloadHandler) {
|
|
1364
|
+
this.beforeUnloadHandler = () => this.disconnect();
|
|
1365
|
+
window.addEventListener('beforeunload', this.beforeUnloadHandler);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Remove the beforeunload handler
|
|
1370
|
+
*/
|
|
1371
|
+
unregisterCleanup() {
|
|
1372
|
+
if (typeof window !== 'undefined' && this.beforeUnloadHandler) {
|
|
1373
|
+
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
|
|
1374
|
+
this.beforeUnloadHandler = null;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/config.ts
|
|
1380
|
+
// Configuration loading and validation
|
|
1381
|
+
/** Default API URL when not specified */
|
|
1382
|
+
const DEFAULT_API_URL = 'https://api.astermind.ai';
|
|
1383
|
+
/**
|
|
1384
|
+
* Derive WebSocket URL from API URL for known SaaS domains.
|
|
1385
|
+
* Maps REST API domains to their WebSocket counterparts:
|
|
1386
|
+
* chatapi.astermind.ai → wss://chatws.astermind.ai
|
|
1387
|
+
* chatapi-dev.astermind.ai → wss://chatws-dev.astermind.ai
|
|
1388
|
+
* api.astermind.ai → wss://chatws.astermind.ai
|
|
1389
|
+
*
|
|
1390
|
+
* Returns undefined for non-SaaS (on-prem) URLs.
|
|
1391
|
+
*/
|
|
1392
|
+
function deriveWsUrl(apiUrl) {
|
|
1393
|
+
try {
|
|
1394
|
+
const url = new URL(apiUrl);
|
|
1395
|
+
// chatapi[-env].astermind.ai → chatws[-env].astermind.ai
|
|
1396
|
+
const match = url.hostname.match(/^chatapi(-[\w]+)?\.astermind\.ai$/);
|
|
1397
|
+
if (match) {
|
|
1398
|
+
const envSuffix = match[1] || '';
|
|
1399
|
+
return `wss://chatws${envSuffix}.astermind.ai`;
|
|
1400
|
+
}
|
|
1401
|
+
// api.astermind.ai → chatws.astermind.ai (default SaaS)
|
|
1402
|
+
if (url.hostname === 'api.astermind.ai') {
|
|
1403
|
+
return 'wss://chatws.astermind.ai';
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
catch {
|
|
1407
|
+
// Invalid URL, no WS derivation
|
|
1408
|
+
}
|
|
1409
|
+
return undefined;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Try to load wsUrl from environment or globals.
|
|
1413
|
+
* Returns the first found wsUrl or undefined.
|
|
1414
|
+
*/
|
|
1415
|
+
function loadWsUrl() {
|
|
1416
|
+
// Vite env var
|
|
1417
|
+
try {
|
|
1418
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1419
|
+
const importMeta = globalThis.import?.meta?.env;
|
|
1420
|
+
if (importMeta?.VITE_ASTERMIND_RAG_WS_URL) {
|
|
1421
|
+
return importMeta.VITE_ASTERMIND_RAG_WS_URL;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
catch { /* not available */ }
|
|
1425
|
+
// Node.js / CRA env var
|
|
1426
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
1427
|
+
if (process.env.ASTERMIND_RAG_WS_URL)
|
|
1428
|
+
return process.env.ASTERMIND_RAG_WS_URL;
|
|
1429
|
+
if (process.env.REACT_APP_ASTERMIND_RAG_WS_URL)
|
|
1430
|
+
return process.env.REACT_APP_ASTERMIND_RAG_WS_URL;
|
|
1431
|
+
}
|
|
1432
|
+
// SSR-injected config
|
|
1433
|
+
if (typeof window !== 'undefined') {
|
|
1434
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1435
|
+
const injected = window.__ASTERMIND_CONFIG__;
|
|
1436
|
+
if (injected?.wsUrl)
|
|
1437
|
+
return injected.wsUrl;
|
|
1438
|
+
// Global config object
|
|
1439
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1440
|
+
const globalConfig = window.astermindConfig;
|
|
1441
|
+
if (globalConfig?.wsUrl)
|
|
1442
|
+
return globalConfig.wsUrl;
|
|
1443
|
+
}
|
|
1444
|
+
// Script data attribute
|
|
1445
|
+
if (typeof document !== 'undefined') {
|
|
1446
|
+
const script = document.querySelector('script[data-astermind-ws-url]');
|
|
1447
|
+
if (script) {
|
|
1448
|
+
const wsUrl = script.getAttribute('data-astermind-ws-url');
|
|
1449
|
+
if (wsUrl)
|
|
1450
|
+
return wsUrl;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return undefined;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Resolve the WebSocket URL for a given config.
|
|
1457
|
+
* Priority: explicit wsUrl → env var → auto-derived from apiUrl
|
|
1458
|
+
*/
|
|
1459
|
+
function resolveWsUrl(config) {
|
|
1460
|
+
// Explicit wsUrl takes priority
|
|
1461
|
+
if (config.wsUrl)
|
|
1462
|
+
return config.wsUrl;
|
|
1463
|
+
// Don't auto-detect if transport is forced to REST
|
|
1464
|
+
if (config.transport === 'rest')
|
|
1465
|
+
return undefined;
|
|
1466
|
+
// Try environment / globals
|
|
1467
|
+
const envWsUrl = loadWsUrl();
|
|
1468
|
+
if (envWsUrl)
|
|
1469
|
+
return envWsUrl;
|
|
1470
|
+
// Auto-derive from apiUrl for known SaaS domains
|
|
1471
|
+
return deriveWsUrl(config.apiUrl);
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Validate configuration
|
|
1475
|
+
*/
|
|
1476
|
+
function validateConfig(config) {
|
|
1477
|
+
if (!config || typeof config !== 'object') {
|
|
1478
|
+
throw new Error('Config must be an object');
|
|
1479
|
+
}
|
|
1480
|
+
const c = config;
|
|
1481
|
+
if (!c.apiUrl || typeof c.apiUrl !== 'string') {
|
|
1482
|
+
throw new Error('apiUrl is required and must be a string');
|
|
1483
|
+
}
|
|
1484
|
+
if (!c.apiKey || typeof c.apiKey !== 'string') {
|
|
1485
|
+
throw new Error('apiKey is required and must be a string');
|
|
1486
|
+
}
|
|
1487
|
+
if (!c.apiKey.startsWith('am_')) {
|
|
1488
|
+
throw new Error('apiKey must start with "am_"');
|
|
1489
|
+
}
|
|
1490
|
+
return true;
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Load config from Node.js process.env (for bundlers that replace process.env)
|
|
1494
|
+
*/
|
|
1495
|
+
function loadFromProcessEnv() {
|
|
1496
|
+
// Check if process.env exists (Node.js or bundler-injected)
|
|
1497
|
+
if (typeof process === 'undefined' || !process.env) {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
// Check for ASTERMIND_RAG_* env vars (non-prefixed, for Node.js/server environments)
|
|
1501
|
+
const apiKey = process.env.ASTERMIND_RAG_API_KEY;
|
|
1502
|
+
const apiUrl = process.env.ASTERMIND_RAG_API_SERVER_URL;
|
|
1503
|
+
if (apiKey) {
|
|
1504
|
+
return {
|
|
1505
|
+
apiKey,
|
|
1506
|
+
apiUrl: apiUrl || DEFAULT_API_URL,
|
|
1507
|
+
_source: 'env'
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
// Check for CRA-style REACT_APP_* env vars
|
|
1511
|
+
const craApiKey = process.env.REACT_APP_ASTERMIND_RAG_API_KEY;
|
|
1512
|
+
const craApiUrl = process.env.REACT_APP_ASTERMIND_RAG_API_SERVER_URL;
|
|
1513
|
+
if (craApiKey) {
|
|
1514
|
+
return {
|
|
1515
|
+
apiKey: craApiKey,
|
|
1516
|
+
apiUrl: craApiUrl || DEFAULT_API_URL,
|
|
1517
|
+
_source: 'env'
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Load config from Vite's import.meta.env (browser environment)
|
|
1524
|
+
* Note: This only works at build time when Vite replaces the variables
|
|
1525
|
+
*/
|
|
1526
|
+
function loadFromViteEnv() {
|
|
1527
|
+
// Check for window.__ASTERMIND_CONFIG__ (SSR-injected config)
|
|
1528
|
+
if (typeof window !== 'undefined') {
|
|
1529
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1530
|
+
const injected = window.__ASTERMIND_CONFIG__;
|
|
1531
|
+
if (injected && typeof injected === 'object' && injected.apiKey) {
|
|
1532
|
+
return {
|
|
1533
|
+
...injected,
|
|
1534
|
+
apiUrl: injected.apiUrl || DEFAULT_API_URL,
|
|
1535
|
+
_source: 'vite'
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
// Check for Vite's import.meta.env (replaced at build time)
|
|
1540
|
+
// Note: TypeScript doesn't know about import.meta.env, so we use a try-catch
|
|
1541
|
+
try {
|
|
1542
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1543
|
+
const importMeta = globalThis.import?.meta?.env;
|
|
1544
|
+
if (importMeta) {
|
|
1545
|
+
const apiKey = importMeta.VITE_ASTERMIND_RAG_API_KEY;
|
|
1546
|
+
const apiUrl = importMeta.VITE_ASTERMIND_RAG_API_SERVER_URL;
|
|
1547
|
+
if (apiKey) {
|
|
1548
|
+
return {
|
|
1549
|
+
apiKey,
|
|
1550
|
+
apiUrl: apiUrl || DEFAULT_API_URL,
|
|
1551
|
+
_source: 'vite'
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
catch {
|
|
1557
|
+
// import.meta not available in this environment
|
|
1558
|
+
}
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Load config from window.astermindConfig global object
|
|
1563
|
+
*/
|
|
1564
|
+
function loadFromGlobalObject() {
|
|
1565
|
+
if (typeof window === 'undefined') {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1569
|
+
const globalConfig = window.astermindConfig;
|
|
1570
|
+
if (globalConfig && typeof globalConfig === 'object' && globalConfig.apiKey) {
|
|
1571
|
+
return {
|
|
1572
|
+
...globalConfig,
|
|
1573
|
+
apiUrl: globalConfig.apiUrl || DEFAULT_API_URL,
|
|
1574
|
+
_source: 'window'
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Load config from script tag data attributes
|
|
1581
|
+
*/
|
|
1582
|
+
function loadFromScriptAttributes() {
|
|
1583
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
const script = document.querySelector('script[data-astermind-key]');
|
|
1587
|
+
if (script) {
|
|
1588
|
+
const apiKey = script.getAttribute('data-astermind-key');
|
|
1589
|
+
if (apiKey) {
|
|
1590
|
+
return {
|
|
1591
|
+
apiKey,
|
|
1592
|
+
apiUrl: script.getAttribute('data-astermind-url') || DEFAULT_API_URL,
|
|
1593
|
+
_source: 'data-attr'
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Load config using priority-based fallback chain:
|
|
1601
|
+
* 1. Environment variables (process.env - for bundlers/Node.js)
|
|
1602
|
+
* 2. Vite environment variables (import.meta.env or window.__ASTERMIND_CONFIG__)
|
|
1603
|
+
* 3. Global object (window.astermindConfig)
|
|
1604
|
+
* 4. Script data attributes (data-astermind-key, data-astermind-url)
|
|
1605
|
+
*
|
|
1606
|
+
* @param options - Configuration options
|
|
1607
|
+
* @param options.throwOnMissingKey - If true (default), throws when no API key found. If false, returns null and logs a warning.
|
|
1608
|
+
* @returns Configuration object or null if not found and throwOnMissingKey is false
|
|
1609
|
+
*/
|
|
1610
|
+
function loadConfig(options = {}) {
|
|
1611
|
+
const { throwOnMissingKey = true } = options;
|
|
1612
|
+
// Priority 1: Environment variables (Node.js/bundler)
|
|
1613
|
+
const envConfig = loadFromProcessEnv();
|
|
1614
|
+
if (envConfig) {
|
|
1615
|
+
return envConfig;
|
|
1616
|
+
}
|
|
1617
|
+
// Priority 2: Vite environment variables / SSR-injected config
|
|
1618
|
+
const viteConfig = loadFromViteEnv();
|
|
1619
|
+
if (viteConfig) {
|
|
1620
|
+
return viteConfig;
|
|
1621
|
+
}
|
|
1622
|
+
// Priority 3: Global object (window.astermindConfig)
|
|
1623
|
+
const globalConfig = loadFromGlobalObject();
|
|
1624
|
+
if (globalConfig) {
|
|
1625
|
+
validateConfig(globalConfig);
|
|
1626
|
+
return globalConfig;
|
|
1627
|
+
}
|
|
1628
|
+
// Priority 4: Script data attributes
|
|
1629
|
+
const scriptConfig = loadFromScriptAttributes();
|
|
1630
|
+
if (scriptConfig) {
|
|
1631
|
+
return scriptConfig;
|
|
1006
1632
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1633
|
+
// No config found
|
|
1634
|
+
if (throwOnMissingKey) {
|
|
1635
|
+
throw new Error('AsterMind API key is required. Configure using one of these methods:\n' +
|
|
1636
|
+
' 1. Set VITE_ASTERMIND_RAG_API_KEY environment variable (Vite)\n' +
|
|
1637
|
+
' 2. Set REACT_APP_ASTERMIND_RAG_API_KEY environment variable (CRA)\n' +
|
|
1638
|
+
' 3. Set window.astermindConfig = { apiKey: "am_...", apiUrl: "..." }\n' +
|
|
1639
|
+
' 4. Add data-astermind-key attribute to your script tag\n' +
|
|
1640
|
+
' 5. Pass apiKey directly to createClient() or CyberneticClient constructor');
|
|
1641
|
+
}
|
|
1642
|
+
else {
|
|
1643
|
+
console.warn('[AsterMind] No API key found. Chatbot will not function until configured.');
|
|
1644
|
+
return null;
|
|
1016
1645
|
}
|
|
1017
1646
|
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Export individual loaders for testing and advanced use cases
|
|
1649
|
+
*/
|
|
1650
|
+
const configLoaders = {
|
|
1651
|
+
loadFromProcessEnv,
|
|
1652
|
+
loadFromViteEnv,
|
|
1653
|
+
loadFromGlobalObject,
|
|
1654
|
+
loadFromScriptAttributes
|
|
1655
|
+
};
|
|
1018
1656
|
|
|
1019
1657
|
// src/license/base64url.ts
|
|
1020
1658
|
// Base64URL encoding/decoding utilities for JWT handling
|
|
@@ -1651,6 +2289,7 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
|
|
|
1651
2289
|
*/
|
|
1652
2290
|
class CyberneticClient {
|
|
1653
2291
|
constructor(config) {
|
|
2292
|
+
this.wsTransport = null;
|
|
1654
2293
|
this.status = 'connecting';
|
|
1655
2294
|
this.lastError = null;
|
|
1656
2295
|
// Maintenance mode tracking (ADR-200)
|
|
@@ -1665,10 +2304,15 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
|
|
|
1665
2304
|
this.omegaRAG = null;
|
|
1666
2305
|
// Track if offline warning has been shown
|
|
1667
2306
|
this.offlineWarningShown = false;
|
|
2307
|
+
// Resolve WebSocket URL (explicit → env → auto-derived from apiUrl)
|
|
2308
|
+
const wsUrl = resolveWsUrl(config);
|
|
2309
|
+
const transport = config.transport ?? 'auto';
|
|
1668
2310
|
// Apply defaults
|
|
1669
2311
|
this.config = {
|
|
1670
2312
|
apiUrl: config.apiUrl,
|
|
1671
2313
|
apiKey: config.apiKey,
|
|
2314
|
+
wsUrl,
|
|
2315
|
+
transport,
|
|
1672
2316
|
fallback: {
|
|
1673
2317
|
enabled: config.fallback?.enabled ?? true,
|
|
1674
2318
|
cacheMaxAge: config.fallback?.cacheMaxAge ?? 86400000, // 24 hours
|
|
@@ -1711,6 +2355,11 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
|
|
|
1711
2355
|
}).catch(() => {
|
|
1712
2356
|
// License verification errors are handled internally
|
|
1713
2357
|
});
|
|
2358
|
+
// Initialize WebSocket transport if configured and not forced to REST
|
|
2359
|
+
if (wsUrl && transport !== 'rest' && typeof WebSocket !== 'undefined') {
|
|
2360
|
+
this.wsTransport = new WebSocketTransport(wsUrl, config.apiKey, config.websocket);
|
|
2361
|
+
console.log(`[Cybernetic] WebSocket transport enabled: ${wsUrl}`);
|
|
2362
|
+
}
|
|
1714
2363
|
// Monitor connection status
|
|
1715
2364
|
this.monitorConnection();
|
|
1716
2365
|
// Pre-cache documents on init if enabled
|
|
@@ -2089,6 +2738,60 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
|
|
|
2089
2738
|
callbacks.onComplete?.(response);
|
|
2090
2739
|
return;
|
|
2091
2740
|
}
|
|
2741
|
+
// Try WebSocket transport first (if available and not forced to REST)
|
|
2742
|
+
if (this.wsTransport && this.config.transport !== 'rest') {
|
|
2743
|
+
try {
|
|
2744
|
+
await this.wsTransport.chatStream(message, {
|
|
2745
|
+
sessionId: options?.sessionId,
|
|
2746
|
+
context: options?.context,
|
|
2747
|
+
onToken: callbacks.onToken,
|
|
2748
|
+
onSources: callbacks.onSources,
|
|
2749
|
+
onComplete: (response) => {
|
|
2750
|
+
this.setStatus('online');
|
|
2751
|
+
// Process through license manager
|
|
2752
|
+
const processedReply = this.licenseManager.processResponse(response.reply);
|
|
2753
|
+
callbacks.onComplete?.({
|
|
2754
|
+
...response,
|
|
2755
|
+
reply: processedReply
|
|
2756
|
+
});
|
|
2757
|
+
},
|
|
2758
|
+
onError: (error) => {
|
|
2759
|
+
// In 'auto' mode, fall back to SSE on WS error
|
|
2760
|
+
if (this.config.transport === 'auto') {
|
|
2761
|
+
console.warn('[Cybernetic] WebSocket error, falling back to SSE:', error.message);
|
|
2762
|
+
this.streamViaSSE(message, callbacks, options);
|
|
2763
|
+
}
|
|
2764
|
+
else {
|
|
2765
|
+
// 'websocket' mode — no fallback
|
|
2766
|
+
const normalizedError = this.normalizeError(error);
|
|
2767
|
+
this.config.onError(normalizedError);
|
|
2768
|
+
callbacks.onError?.(normalizedError);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
});
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
catch (error) {
|
|
2775
|
+
if (this.config.transport === 'websocket') {
|
|
2776
|
+
// Forced WebSocket mode — don't fall back
|
|
2777
|
+
const normalizedError = this.normalizeError(error);
|
|
2778
|
+
this.lastError = normalizedError;
|
|
2779
|
+
this.config.onError(normalizedError);
|
|
2780
|
+
callbacks.onError?.(normalizedError);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
// 'auto' mode: fall through to SSE
|
|
2784
|
+
console.warn('[Cybernetic] WebSocket connect failed, using SSE fallback');
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
// REST+SSE path (on-prem, or fallback from WebSocket)
|
|
2788
|
+
await this.streamViaSSE(message, callbacks, options);
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Stream chat via REST+SSE (original transport).
|
|
2792
|
+
* Used as the primary transport for on-prem, or as fallback for SaaS WebSocket failures.
|
|
2793
|
+
*/
|
|
2794
|
+
async streamViaSSE(message, callbacks, options) {
|
|
2092
2795
|
try {
|
|
2093
2796
|
await this.apiClient.chatStream(message, {
|
|
2094
2797
|
sessionId: options?.sessionId,
|
|
@@ -2172,6 +2875,16 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
|
|
|
2172
2875
|
await this.cache.clear();
|
|
2173
2876
|
this.localRAG.reset();
|
|
2174
2877
|
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Clean up all resources (WebSocket connection, caches, event listeners).
|
|
2880
|
+
* Call this when the client is no longer needed to prevent memory leaks.
|
|
2881
|
+
*/
|
|
2882
|
+
destroy() {
|
|
2883
|
+
if (this.wsTransport) {
|
|
2884
|
+
this.wsTransport.disconnect();
|
|
2885
|
+
this.wsTransport = null;
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2175
2888
|
/**
|
|
2176
2889
|
* Manually check if backend is reachable
|
|
2177
2890
|
*/
|
|
@@ -2482,194 +3195,6 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
|
|
|
2482
3195
|
}
|
|
2483
3196
|
}
|
|
2484
3197
|
|
|
2485
|
-
// src/config.ts
|
|
2486
|
-
// Configuration loading and validation
|
|
2487
|
-
/** Default API URL when not specified */
|
|
2488
|
-
const DEFAULT_API_URL = 'https://api.astermind.ai';
|
|
2489
|
-
/**
|
|
2490
|
-
* Validate configuration
|
|
2491
|
-
*/
|
|
2492
|
-
function validateConfig(config) {
|
|
2493
|
-
if (!config || typeof config !== 'object') {
|
|
2494
|
-
throw new Error('Config must be an object');
|
|
2495
|
-
}
|
|
2496
|
-
const c = config;
|
|
2497
|
-
if (!c.apiUrl || typeof c.apiUrl !== 'string') {
|
|
2498
|
-
throw new Error('apiUrl is required and must be a string');
|
|
2499
|
-
}
|
|
2500
|
-
if (!c.apiKey || typeof c.apiKey !== 'string') {
|
|
2501
|
-
throw new Error('apiKey is required and must be a string');
|
|
2502
|
-
}
|
|
2503
|
-
if (!c.apiKey.startsWith('am_')) {
|
|
2504
|
-
throw new Error('apiKey must start with "am_"');
|
|
2505
|
-
}
|
|
2506
|
-
return true;
|
|
2507
|
-
}
|
|
2508
|
-
/**
|
|
2509
|
-
* Load config from Node.js process.env (for bundlers that replace process.env)
|
|
2510
|
-
*/
|
|
2511
|
-
function loadFromProcessEnv() {
|
|
2512
|
-
// Check if process.env exists (Node.js or bundler-injected)
|
|
2513
|
-
if (typeof process === 'undefined' || !process.env) {
|
|
2514
|
-
return null;
|
|
2515
|
-
}
|
|
2516
|
-
// Check for ASTERMIND_RAG_* env vars (non-prefixed, for Node.js/server environments)
|
|
2517
|
-
const apiKey = process.env.ASTERMIND_RAG_API_KEY;
|
|
2518
|
-
const apiUrl = process.env.ASTERMIND_RAG_API_SERVER_URL;
|
|
2519
|
-
if (apiKey) {
|
|
2520
|
-
return {
|
|
2521
|
-
apiKey,
|
|
2522
|
-
apiUrl: apiUrl || DEFAULT_API_URL,
|
|
2523
|
-
_source: 'env'
|
|
2524
|
-
};
|
|
2525
|
-
}
|
|
2526
|
-
// Check for CRA-style REACT_APP_* env vars
|
|
2527
|
-
const craApiKey = process.env.REACT_APP_ASTERMIND_RAG_API_KEY;
|
|
2528
|
-
const craApiUrl = process.env.REACT_APP_ASTERMIND_RAG_API_SERVER_URL;
|
|
2529
|
-
if (craApiKey) {
|
|
2530
|
-
return {
|
|
2531
|
-
apiKey: craApiKey,
|
|
2532
|
-
apiUrl: craApiUrl || DEFAULT_API_URL,
|
|
2533
|
-
_source: 'env'
|
|
2534
|
-
};
|
|
2535
|
-
}
|
|
2536
|
-
return null;
|
|
2537
|
-
}
|
|
2538
|
-
/**
|
|
2539
|
-
* Load config from Vite's import.meta.env (browser environment)
|
|
2540
|
-
* Note: This only works at build time when Vite replaces the variables
|
|
2541
|
-
*/
|
|
2542
|
-
function loadFromViteEnv() {
|
|
2543
|
-
// Check for window.__ASTERMIND_CONFIG__ (SSR-injected config)
|
|
2544
|
-
if (typeof window !== 'undefined') {
|
|
2545
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2546
|
-
const injected = window.__ASTERMIND_CONFIG__;
|
|
2547
|
-
if (injected && typeof injected === 'object' && injected.apiKey) {
|
|
2548
|
-
return {
|
|
2549
|
-
...injected,
|
|
2550
|
-
apiUrl: injected.apiUrl || DEFAULT_API_URL,
|
|
2551
|
-
_source: 'vite'
|
|
2552
|
-
};
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
// Check for Vite's import.meta.env (replaced at build time)
|
|
2556
|
-
// Note: TypeScript doesn't know about import.meta.env, so we use a try-catch
|
|
2557
|
-
try {
|
|
2558
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2559
|
-
const importMeta = globalThis.import?.meta?.env;
|
|
2560
|
-
if (importMeta) {
|
|
2561
|
-
const apiKey = importMeta.VITE_ASTERMIND_RAG_API_KEY;
|
|
2562
|
-
const apiUrl = importMeta.VITE_ASTERMIND_RAG_API_SERVER_URL;
|
|
2563
|
-
if (apiKey) {
|
|
2564
|
-
return {
|
|
2565
|
-
apiKey,
|
|
2566
|
-
apiUrl: apiUrl || DEFAULT_API_URL,
|
|
2567
|
-
_source: 'vite'
|
|
2568
|
-
};
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
}
|
|
2572
|
-
catch {
|
|
2573
|
-
// import.meta not available in this environment
|
|
2574
|
-
}
|
|
2575
|
-
return null;
|
|
2576
|
-
}
|
|
2577
|
-
/**
|
|
2578
|
-
* Load config from window.astermindConfig global object
|
|
2579
|
-
*/
|
|
2580
|
-
function loadFromGlobalObject() {
|
|
2581
|
-
if (typeof window === 'undefined') {
|
|
2582
|
-
return null;
|
|
2583
|
-
}
|
|
2584
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2585
|
-
const globalConfig = window.astermindConfig;
|
|
2586
|
-
if (globalConfig && typeof globalConfig === 'object' && globalConfig.apiKey) {
|
|
2587
|
-
return {
|
|
2588
|
-
...globalConfig,
|
|
2589
|
-
apiUrl: globalConfig.apiUrl || DEFAULT_API_URL,
|
|
2590
|
-
_source: 'window'
|
|
2591
|
-
};
|
|
2592
|
-
}
|
|
2593
|
-
return null;
|
|
2594
|
-
}
|
|
2595
|
-
/**
|
|
2596
|
-
* Load config from script tag data attributes
|
|
2597
|
-
*/
|
|
2598
|
-
function loadFromScriptAttributes() {
|
|
2599
|
-
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
2600
|
-
return null;
|
|
2601
|
-
}
|
|
2602
|
-
const script = document.querySelector('script[data-astermind-key]');
|
|
2603
|
-
if (script) {
|
|
2604
|
-
const apiKey = script.getAttribute('data-astermind-key');
|
|
2605
|
-
if (apiKey) {
|
|
2606
|
-
return {
|
|
2607
|
-
apiKey,
|
|
2608
|
-
apiUrl: script.getAttribute('data-astermind-url') || DEFAULT_API_URL,
|
|
2609
|
-
_source: 'data-attr'
|
|
2610
|
-
};
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
return null;
|
|
2614
|
-
}
|
|
2615
|
-
/**
|
|
2616
|
-
* Load config using priority-based fallback chain:
|
|
2617
|
-
* 1. Environment variables (process.env - for bundlers/Node.js)
|
|
2618
|
-
* 2. Vite environment variables (import.meta.env or window.__ASTERMIND_CONFIG__)
|
|
2619
|
-
* 3. Global object (window.astermindConfig)
|
|
2620
|
-
* 4. Script data attributes (data-astermind-key, data-astermind-url)
|
|
2621
|
-
*
|
|
2622
|
-
* @param options - Configuration options
|
|
2623
|
-
* @param options.throwOnMissingKey - If true (default), throws when no API key found. If false, returns null and logs a warning.
|
|
2624
|
-
* @returns Configuration object or null if not found and throwOnMissingKey is false
|
|
2625
|
-
*/
|
|
2626
|
-
function loadConfig(options = {}) {
|
|
2627
|
-
const { throwOnMissingKey = true } = options;
|
|
2628
|
-
// Priority 1: Environment variables (Node.js/bundler)
|
|
2629
|
-
const envConfig = loadFromProcessEnv();
|
|
2630
|
-
if (envConfig) {
|
|
2631
|
-
return envConfig;
|
|
2632
|
-
}
|
|
2633
|
-
// Priority 2: Vite environment variables / SSR-injected config
|
|
2634
|
-
const viteConfig = loadFromViteEnv();
|
|
2635
|
-
if (viteConfig) {
|
|
2636
|
-
return viteConfig;
|
|
2637
|
-
}
|
|
2638
|
-
// Priority 3: Global object (window.astermindConfig)
|
|
2639
|
-
const globalConfig = loadFromGlobalObject();
|
|
2640
|
-
if (globalConfig) {
|
|
2641
|
-
validateConfig(globalConfig);
|
|
2642
|
-
return globalConfig;
|
|
2643
|
-
}
|
|
2644
|
-
// Priority 4: Script data attributes
|
|
2645
|
-
const scriptConfig = loadFromScriptAttributes();
|
|
2646
|
-
if (scriptConfig) {
|
|
2647
|
-
return scriptConfig;
|
|
2648
|
-
}
|
|
2649
|
-
// No config found
|
|
2650
|
-
if (throwOnMissingKey) {
|
|
2651
|
-
throw new Error('AsterMind API key is required. Configure using one of these methods:\n' +
|
|
2652
|
-
' 1. Set VITE_ASTERMIND_RAG_API_KEY environment variable (Vite)\n' +
|
|
2653
|
-
' 2. Set REACT_APP_ASTERMIND_RAG_API_KEY environment variable (CRA)\n' +
|
|
2654
|
-
' 3. Set window.astermindConfig = { apiKey: "am_...", apiUrl: "..." }\n' +
|
|
2655
|
-
' 4. Add data-astermind-key attribute to your script tag\n' +
|
|
2656
|
-
' 5. Pass apiKey directly to createClient() or CyberneticClient constructor');
|
|
2657
|
-
}
|
|
2658
|
-
else {
|
|
2659
|
-
console.warn('[AsterMind] No API key found. Chatbot will not function until configured.');
|
|
2660
|
-
return null;
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
|
-
/**
|
|
2664
|
-
* Export individual loaders for testing and advanced use cases
|
|
2665
|
-
*/
|
|
2666
|
-
const configLoaders = {
|
|
2667
|
-
loadFromProcessEnv,
|
|
2668
|
-
loadFromViteEnv,
|
|
2669
|
-
loadFromGlobalObject,
|
|
2670
|
-
loadFromScriptAttributes
|
|
2671
|
-
};
|
|
2672
|
-
|
|
2673
3198
|
// src/agentic/SiteMapDiscovery.ts
|
|
2674
3199
|
// Multi-source sitemap discovery and merging for agentic navigation
|
|
2675
3200
|
// Zero-config mode: automatically discovers routes on any site
|
|
@@ -6665,16 +7190,19 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
|
|
|
6665
7190
|
exports.OmegaOfflineRAG = OmegaOfflineRAG;
|
|
6666
7191
|
exports.REQUIRED_FEATURES = REQUIRED_FEATURES;
|
|
6667
7192
|
exports.SiteMapDiscovery = SiteMapDiscovery;
|
|
7193
|
+
exports.WebSocketTransport = WebSocketTransport;
|
|
6668
7194
|
exports.configLoaders = configLoaders;
|
|
6669
7195
|
exports.createClient = createClient;
|
|
6670
7196
|
exports.createDiscoveryConfig = createDiscoveryConfig;
|
|
6671
7197
|
exports.createLicenseManager = createLicenseManager;
|
|
7198
|
+
exports.deriveWsUrl = deriveWsUrl;
|
|
6672
7199
|
exports.detectEnvironment = detectEnvironment;
|
|
6673
7200
|
exports.getEnforcementMode = getEnforcementMode;
|
|
6674
7201
|
exports.getTokenExpiration = getTokenExpiration;
|
|
6675
7202
|
exports.isValidJWTFormat = isValidJWTFormat;
|
|
6676
7203
|
exports.loadConfig = loadConfig;
|
|
6677
7204
|
exports.registerAgenticCapabilities = registerAgenticCapabilities;
|
|
7205
|
+
exports.resolveWsUrl = resolveWsUrl;
|
|
6678
7206
|
exports.useSiteMapDiscovery = useSiteMapDiscovery;
|
|
6679
7207
|
exports.validateConfig = validateConfig;
|
|
6680
7208
|
exports.verifyLicenseToken = verifyLicenseToken;
|