@astermind/cybernetic-chatbot-client 1.0.6
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/LICENSE +21 -0
- package/README.md +667 -0
- package/dist/ApiClient.d.ts +86 -0
- package/dist/ApiClient.d.ts.map +1 -0
- package/dist/CyberneticCache.d.ts +56 -0
- package/dist/CyberneticCache.d.ts.map +1 -0
- package/dist/CyberneticClient.d.ts +207 -0
- package/dist/CyberneticClient.d.ts.map +1 -0
- package/dist/CyberneticLocalRAG.d.ts +59 -0
- package/dist/CyberneticLocalRAG.d.ts.map +1 -0
- package/dist/agentic/CyberneticAgent.d.ts +111 -0
- package/dist/agentic/CyberneticAgent.d.ts.map +1 -0
- package/dist/agentic/CyberneticIntentClassifier.d.ts +78 -0
- package/dist/agentic/CyberneticIntentClassifier.d.ts.map +1 -0
- package/dist/agentic/index.d.ts +13 -0
- package/dist/agentic/index.d.ts.map +1 -0
- package/dist/agentic/register.d.ts +32 -0
- package/dist/agentic/register.d.ts.map +1 -0
- package/dist/agentic/tools/ClickTool.d.ts +41 -0
- package/dist/agentic/tools/ClickTool.d.ts.map +1 -0
- package/dist/agentic/tools/FillTool.d.ts +59 -0
- package/dist/agentic/tools/FillTool.d.ts.map +1 -0
- package/dist/agentic/tools/NavigateTool.d.ts +87 -0
- package/dist/agentic/tools/NavigateTool.d.ts.map +1 -0
- package/dist/agentic/tools/ScrollTool.d.ts +74 -0
- package/dist/agentic/tools/ScrollTool.d.ts.map +1 -0
- package/dist/agentic/tools/index.d.ts +9 -0
- package/dist/agentic/tools/index.d.ts.map +1 -0
- package/dist/agentic/types.d.ts +112 -0
- package/dist/agentic/types.d.ts.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/cybernetic-chatbot-client-full.esm.js +3271 -0
- package/dist/cybernetic-chatbot-client-full.esm.js.map +1 -0
- package/dist/cybernetic-chatbot-client-full.min.js +2 -0
- package/dist/cybernetic-chatbot-client-full.min.js.map +1 -0
- package/dist/cybernetic-chatbot-client-full.umd.js +3296 -0
- package/dist/cybernetic-chatbot-client-full.umd.js.map +1 -0
- package/dist/cybernetic-chatbot-client.esm.js +3265 -0
- package/dist/cybernetic-chatbot-client.esm.js.map +1 -0
- package/dist/cybernetic-chatbot-client.min.js +2 -0
- package/dist/cybernetic-chatbot-client.min.js.map +1 -0
- package/dist/cybernetic-chatbot-client.umd.js +3290 -0
- package/dist/cybernetic-chatbot-client.umd.js.map +1 -0
- package/dist/full.d.ts +15 -0
- package/dist/full.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/license/base64url.d.ts +24 -0
- package/dist/license/base64url.d.ts.map +1 -0
- package/dist/license/index.d.ts +5 -0
- package/dist/license/index.d.ts.map +1 -0
- package/dist/license/licenseManager.d.ts +124 -0
- package/dist/license/licenseManager.d.ts.map +1 -0
- package/dist/license/types.d.ts +72 -0
- package/dist/license/types.d.ts.map +1 -0
- package/dist/license/verifier.d.ts +19 -0
- package/dist/license/verifier.d.ts.map +1 -0
- package/dist/types.d.ts +163 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,3290 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.AsterMindCybernetic = {}));
|
|
5
|
+
})(this, (function (exports) { 'use strict';
|
|
6
|
+
|
|
7
|
+
// src/ApiClient.ts
|
|
8
|
+
// HTTP client for AsterMind backend API
|
|
9
|
+
/**
|
|
10
|
+
* HTTP/SSE client for AsterMind backend
|
|
11
|
+
*/
|
|
12
|
+
class ApiClient {
|
|
13
|
+
constructor(baseUrl, apiKey) {
|
|
14
|
+
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
15
|
+
this.apiKey = apiKey;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Send chat message and get complete response
|
|
19
|
+
*/
|
|
20
|
+
async chat(message, options) {
|
|
21
|
+
const response = await fetch(`${this.baseUrl}/api/external/chat`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'X-API-Key': this.apiKey
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
message,
|
|
29
|
+
sessionId: options?.sessionId,
|
|
30
|
+
context: options?.context
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const error = await response.json().catch(() => ({}));
|
|
35
|
+
throw new Error(error.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
36
|
+
}
|
|
37
|
+
return response.json();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Send chat message with streaming response via SSE
|
|
41
|
+
*/
|
|
42
|
+
async chatStream(message, options) {
|
|
43
|
+
const response = await fetch(`${this.baseUrl}/api/external/chat/stream`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
'X-API-Key': this.apiKey
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
message,
|
|
51
|
+
sessionId: options?.sessionId,
|
|
52
|
+
context: options?.context
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const error = await response.json().catch(() => ({}));
|
|
57
|
+
throw new Error(error.message || `HTTP ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
const reader = response.body?.getReader();
|
|
60
|
+
if (!reader) {
|
|
61
|
+
throw new Error('Streaming not supported');
|
|
62
|
+
}
|
|
63
|
+
const decoder = new TextDecoder();
|
|
64
|
+
let buffer = '';
|
|
65
|
+
let fullText = '';
|
|
66
|
+
let sources = [];
|
|
67
|
+
let sessionId;
|
|
68
|
+
try {
|
|
69
|
+
while (true) {
|
|
70
|
+
const { done, value } = await reader.read();
|
|
71
|
+
if (done)
|
|
72
|
+
break;
|
|
73
|
+
buffer += decoder.decode(value, { stream: true });
|
|
74
|
+
const lines = buffer.split('\n');
|
|
75
|
+
buffer = lines.pop() || '';
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
if (line.startsWith('event: ')) {
|
|
78
|
+
// Event type handled in data line
|
|
79
|
+
}
|
|
80
|
+
else if (line.startsWith('data: ')) {
|
|
81
|
+
try {
|
|
82
|
+
const data = JSON.parse(line.slice(6));
|
|
83
|
+
if (data.text !== undefined) {
|
|
84
|
+
// Token event
|
|
85
|
+
fullText += data.text;
|
|
86
|
+
options.onToken?.(data.text);
|
|
87
|
+
}
|
|
88
|
+
else if (data.sources !== undefined) {
|
|
89
|
+
// Sources event
|
|
90
|
+
sources = data.sources;
|
|
91
|
+
options.onSources?.(sources);
|
|
92
|
+
}
|
|
93
|
+
else if (data.sessionId !== undefined) {
|
|
94
|
+
// Done event
|
|
95
|
+
sessionId = data.sessionId;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Skip malformed JSON
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
options.onComplete?.({ fullText, sessionId, sources });
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
options.onError?.(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get general documents for caching
|
|
112
|
+
*/
|
|
113
|
+
async getGeneralDocs(since) {
|
|
114
|
+
const params = new URLSearchParams();
|
|
115
|
+
if (since) {
|
|
116
|
+
params.set('since', since);
|
|
117
|
+
}
|
|
118
|
+
const url = `${this.baseUrl}/api/external/docs${params.toString() ? '?' + params : ''}`;
|
|
119
|
+
const response = await fetch(url, {
|
|
120
|
+
headers: {
|
|
121
|
+
'X-API-Key': this.apiKey
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(`HTTP ${response.status}`);
|
|
126
|
+
}
|
|
127
|
+
const data = await response.json();
|
|
128
|
+
return data.documents;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get API status, quota, and system settings
|
|
132
|
+
*/
|
|
133
|
+
async getStatus() {
|
|
134
|
+
const response = await fetch(`${this.baseUrl}/api/external/status`, {
|
|
135
|
+
headers: {
|
|
136
|
+
'X-API-Key': this.apiKey
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new Error(`HTTP ${response.status}`);
|
|
141
|
+
}
|
|
142
|
+
return response.json();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Health check (no auth required)
|
|
146
|
+
*/
|
|
147
|
+
async health() {
|
|
148
|
+
const response = await fetch(`${this.baseUrl}/api/external/health`);
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(`HTTP ${response.status}`);
|
|
151
|
+
}
|
|
152
|
+
return response.json();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
|
|
157
|
+
|
|
158
|
+
let idbProxyableTypes;
|
|
159
|
+
let cursorAdvanceMethods;
|
|
160
|
+
// This is a function to prevent it throwing up in node environments.
|
|
161
|
+
function getIdbProxyableTypes() {
|
|
162
|
+
return (idbProxyableTypes ||
|
|
163
|
+
(idbProxyableTypes = [
|
|
164
|
+
IDBDatabase,
|
|
165
|
+
IDBObjectStore,
|
|
166
|
+
IDBIndex,
|
|
167
|
+
IDBCursor,
|
|
168
|
+
IDBTransaction,
|
|
169
|
+
]));
|
|
170
|
+
}
|
|
171
|
+
// This is a function to prevent it throwing up in node environments.
|
|
172
|
+
function getCursorAdvanceMethods() {
|
|
173
|
+
return (cursorAdvanceMethods ||
|
|
174
|
+
(cursorAdvanceMethods = [
|
|
175
|
+
IDBCursor.prototype.advance,
|
|
176
|
+
IDBCursor.prototype.continue,
|
|
177
|
+
IDBCursor.prototype.continuePrimaryKey,
|
|
178
|
+
]));
|
|
179
|
+
}
|
|
180
|
+
const cursorRequestMap = new WeakMap();
|
|
181
|
+
const transactionDoneMap = new WeakMap();
|
|
182
|
+
const transactionStoreNamesMap = new WeakMap();
|
|
183
|
+
const transformCache = new WeakMap();
|
|
184
|
+
const reverseTransformCache = new WeakMap();
|
|
185
|
+
function promisifyRequest(request) {
|
|
186
|
+
const promise = new Promise((resolve, reject) => {
|
|
187
|
+
const unlisten = () => {
|
|
188
|
+
request.removeEventListener('success', success);
|
|
189
|
+
request.removeEventListener('error', error);
|
|
190
|
+
};
|
|
191
|
+
const success = () => {
|
|
192
|
+
resolve(wrap(request.result));
|
|
193
|
+
unlisten();
|
|
194
|
+
};
|
|
195
|
+
const error = () => {
|
|
196
|
+
reject(request.error);
|
|
197
|
+
unlisten();
|
|
198
|
+
};
|
|
199
|
+
request.addEventListener('success', success);
|
|
200
|
+
request.addEventListener('error', error);
|
|
201
|
+
});
|
|
202
|
+
promise
|
|
203
|
+
.then((value) => {
|
|
204
|
+
// Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval
|
|
205
|
+
// (see wrapFunction).
|
|
206
|
+
if (value instanceof IDBCursor) {
|
|
207
|
+
cursorRequestMap.set(value, request);
|
|
208
|
+
}
|
|
209
|
+
// Catching to avoid "Uncaught Promise exceptions"
|
|
210
|
+
})
|
|
211
|
+
.catch(() => { });
|
|
212
|
+
// This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This
|
|
213
|
+
// is because we create many promises from a single IDBRequest.
|
|
214
|
+
reverseTransformCache.set(promise, request);
|
|
215
|
+
return promise;
|
|
216
|
+
}
|
|
217
|
+
function cacheDonePromiseForTransaction(tx) {
|
|
218
|
+
// Early bail if we've already created a done promise for this transaction.
|
|
219
|
+
if (transactionDoneMap.has(tx))
|
|
220
|
+
return;
|
|
221
|
+
const done = new Promise((resolve, reject) => {
|
|
222
|
+
const unlisten = () => {
|
|
223
|
+
tx.removeEventListener('complete', complete);
|
|
224
|
+
tx.removeEventListener('error', error);
|
|
225
|
+
tx.removeEventListener('abort', error);
|
|
226
|
+
};
|
|
227
|
+
const complete = () => {
|
|
228
|
+
resolve();
|
|
229
|
+
unlisten();
|
|
230
|
+
};
|
|
231
|
+
const error = () => {
|
|
232
|
+
reject(tx.error || new DOMException('AbortError', 'AbortError'));
|
|
233
|
+
unlisten();
|
|
234
|
+
};
|
|
235
|
+
tx.addEventListener('complete', complete);
|
|
236
|
+
tx.addEventListener('error', error);
|
|
237
|
+
tx.addEventListener('abort', error);
|
|
238
|
+
});
|
|
239
|
+
// Cache it for later retrieval.
|
|
240
|
+
transactionDoneMap.set(tx, done);
|
|
241
|
+
}
|
|
242
|
+
let idbProxyTraps = {
|
|
243
|
+
get(target, prop, receiver) {
|
|
244
|
+
if (target instanceof IDBTransaction) {
|
|
245
|
+
// Special handling for transaction.done.
|
|
246
|
+
if (prop === 'done')
|
|
247
|
+
return transactionDoneMap.get(target);
|
|
248
|
+
// Polyfill for objectStoreNames because of Edge.
|
|
249
|
+
if (prop === 'objectStoreNames') {
|
|
250
|
+
return target.objectStoreNames || transactionStoreNamesMap.get(target);
|
|
251
|
+
}
|
|
252
|
+
// Make tx.store return the only store in the transaction, or undefined if there are many.
|
|
253
|
+
if (prop === 'store') {
|
|
254
|
+
return receiver.objectStoreNames[1]
|
|
255
|
+
? undefined
|
|
256
|
+
: receiver.objectStore(receiver.objectStoreNames[0]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Else transform whatever we get back.
|
|
260
|
+
return wrap(target[prop]);
|
|
261
|
+
},
|
|
262
|
+
set(target, prop, value) {
|
|
263
|
+
target[prop] = value;
|
|
264
|
+
return true;
|
|
265
|
+
},
|
|
266
|
+
has(target, prop) {
|
|
267
|
+
if (target instanceof IDBTransaction &&
|
|
268
|
+
(prop === 'done' || prop === 'store')) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return prop in target;
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
function replaceTraps(callback) {
|
|
275
|
+
idbProxyTraps = callback(idbProxyTraps);
|
|
276
|
+
}
|
|
277
|
+
function wrapFunction(func) {
|
|
278
|
+
// Due to expected object equality (which is enforced by the caching in `wrap`), we
|
|
279
|
+
// only create one new func per func.
|
|
280
|
+
// Edge doesn't support objectStoreNames (booo), so we polyfill it here.
|
|
281
|
+
if (func === IDBDatabase.prototype.transaction &&
|
|
282
|
+
!('objectStoreNames' in IDBTransaction.prototype)) {
|
|
283
|
+
return function (storeNames, ...args) {
|
|
284
|
+
const tx = func.call(unwrap(this), storeNames, ...args);
|
|
285
|
+
transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]);
|
|
286
|
+
return wrap(tx);
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Cursor methods are special, as the behaviour is a little more different to standard IDB. In
|
|
290
|
+
// IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the
|
|
291
|
+
// cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense
|
|
292
|
+
// with real promises, so each advance methods returns a new promise for the cursor object, or
|
|
293
|
+
// undefined if the end of the cursor has been reached.
|
|
294
|
+
if (getCursorAdvanceMethods().includes(func)) {
|
|
295
|
+
return function (...args) {
|
|
296
|
+
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
|
|
297
|
+
// the original object.
|
|
298
|
+
func.apply(unwrap(this), args);
|
|
299
|
+
return wrap(cursorRequestMap.get(this));
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return function (...args) {
|
|
303
|
+
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
|
|
304
|
+
// the original object.
|
|
305
|
+
return wrap(func.apply(unwrap(this), args));
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function transformCachableValue(value) {
|
|
309
|
+
if (typeof value === 'function')
|
|
310
|
+
return wrapFunction(value);
|
|
311
|
+
// This doesn't return, it just creates a 'done' promise for the transaction,
|
|
312
|
+
// which is later returned for transaction.done (see idbObjectHandler).
|
|
313
|
+
if (value instanceof IDBTransaction)
|
|
314
|
+
cacheDonePromiseForTransaction(value);
|
|
315
|
+
if (instanceOfAny(value, getIdbProxyableTypes()))
|
|
316
|
+
return new Proxy(value, idbProxyTraps);
|
|
317
|
+
// Return the same value back if we're not going to transform it.
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
function wrap(value) {
|
|
321
|
+
// We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because
|
|
322
|
+
// IDB is weird and a single IDBRequest can yield many responses, so these can't be cached.
|
|
323
|
+
if (value instanceof IDBRequest)
|
|
324
|
+
return promisifyRequest(value);
|
|
325
|
+
// If we've already transformed this value before, reuse the transformed value.
|
|
326
|
+
// This is faster, but it also provides object equality.
|
|
327
|
+
if (transformCache.has(value))
|
|
328
|
+
return transformCache.get(value);
|
|
329
|
+
const newValue = transformCachableValue(value);
|
|
330
|
+
// Not all types are transformed.
|
|
331
|
+
// These may be primitive types, so they can't be WeakMap keys.
|
|
332
|
+
if (newValue !== value) {
|
|
333
|
+
transformCache.set(value, newValue);
|
|
334
|
+
reverseTransformCache.set(newValue, value);
|
|
335
|
+
}
|
|
336
|
+
return newValue;
|
|
337
|
+
}
|
|
338
|
+
const unwrap = (value) => reverseTransformCache.get(value);
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Open a database.
|
|
342
|
+
*
|
|
343
|
+
* @param name Name of the database.
|
|
344
|
+
* @param version Schema version.
|
|
345
|
+
* @param callbacks Additional callbacks.
|
|
346
|
+
*/
|
|
347
|
+
function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
|
|
348
|
+
const request = indexedDB.open(name, version);
|
|
349
|
+
const openPromise = wrap(request);
|
|
350
|
+
if (upgrade) {
|
|
351
|
+
request.addEventListener('upgradeneeded', (event) => {
|
|
352
|
+
upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (blocked) {
|
|
356
|
+
request.addEventListener('blocked', (event) => blocked(
|
|
357
|
+
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
|
|
358
|
+
event.oldVersion, event.newVersion, event));
|
|
359
|
+
}
|
|
360
|
+
openPromise
|
|
361
|
+
.then((db) => {
|
|
362
|
+
if (terminated)
|
|
363
|
+
db.addEventListener('close', () => terminated());
|
|
364
|
+
if (blocking) {
|
|
365
|
+
db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event));
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
.catch(() => { });
|
|
369
|
+
return openPromise;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'];
|
|
373
|
+
const writeMethods = ['put', 'add', 'delete', 'clear'];
|
|
374
|
+
const cachedMethods = new Map();
|
|
375
|
+
function getMethod(target, prop) {
|
|
376
|
+
if (!(target instanceof IDBDatabase &&
|
|
377
|
+
!(prop in target) &&
|
|
378
|
+
typeof prop === 'string')) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (cachedMethods.get(prop))
|
|
382
|
+
return cachedMethods.get(prop);
|
|
383
|
+
const targetFuncName = prop.replace(/FromIndex$/, '');
|
|
384
|
+
const useIndex = prop !== targetFuncName;
|
|
385
|
+
const isWrite = writeMethods.includes(targetFuncName);
|
|
386
|
+
if (
|
|
387
|
+
// Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
|
|
388
|
+
!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) ||
|
|
389
|
+
!(isWrite || readMethods.includes(targetFuncName))) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const method = async function (storeName, ...args) {
|
|
393
|
+
// isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :(
|
|
394
|
+
const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly');
|
|
395
|
+
let target = tx.store;
|
|
396
|
+
if (useIndex)
|
|
397
|
+
target = target.index(args.shift());
|
|
398
|
+
// Must reject if op rejects.
|
|
399
|
+
// If it's a write operation, must reject if tx.done rejects.
|
|
400
|
+
// Must reject with op rejection first.
|
|
401
|
+
// Must resolve with op value.
|
|
402
|
+
// Must handle both promises (no unhandled rejections)
|
|
403
|
+
return (await Promise.all([
|
|
404
|
+
target[targetFuncName](...args),
|
|
405
|
+
isWrite && tx.done,
|
|
406
|
+
]))[0];
|
|
407
|
+
};
|
|
408
|
+
cachedMethods.set(prop, method);
|
|
409
|
+
return method;
|
|
410
|
+
}
|
|
411
|
+
replaceTraps((oldTraps) => ({
|
|
412
|
+
...oldTraps,
|
|
413
|
+
get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
|
|
414
|
+
has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop),
|
|
415
|
+
}));
|
|
416
|
+
|
|
417
|
+
// src/CyberneticCache.ts
|
|
418
|
+
// IndexedDB cache for offline document storage
|
|
419
|
+
/**
|
|
420
|
+
* Document cache for offline RAG fallback
|
|
421
|
+
*/
|
|
422
|
+
class CyberneticCache {
|
|
423
|
+
constructor(config) {
|
|
424
|
+
this.db = null;
|
|
425
|
+
this.dbPromise = null;
|
|
426
|
+
this.documentCount = 0;
|
|
427
|
+
this.lastSyncAt = null;
|
|
428
|
+
this.config = config;
|
|
429
|
+
if (config.storage === 'indexeddb' && typeof indexedDB !== 'undefined') {
|
|
430
|
+
this.dbPromise = this.initDB();
|
|
431
|
+
}
|
|
432
|
+
else if (config.storage === 'localstorage') {
|
|
433
|
+
// Load cached metadata from localStorage
|
|
434
|
+
this.loadLocalStorageMetadata();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Load metadata from localStorage
|
|
439
|
+
*/
|
|
440
|
+
loadLocalStorageMetadata() {
|
|
441
|
+
try {
|
|
442
|
+
const meta = localStorage.getItem('cybernetic-cache-meta');
|
|
443
|
+
if (meta) {
|
|
444
|
+
const parsed = JSON.parse(meta);
|
|
445
|
+
this.documentCount = parsed.documentCount || 0;
|
|
446
|
+
this.lastSyncAt = parsed.lastSyncAt || null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
// Ignore parse errors
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Initialize IndexedDB
|
|
455
|
+
*/
|
|
456
|
+
async initDB() {
|
|
457
|
+
this.db = await openDB('cybernetic-cache', 1, {
|
|
458
|
+
upgrade(db) {
|
|
459
|
+
// Documents store
|
|
460
|
+
if (!db.objectStoreNames.contains('documents')) {
|
|
461
|
+
const docStore = db.createObjectStore('documents', { keyPath: 'id' });
|
|
462
|
+
docStore.createIndex('by-updated', 'updatedAt');
|
|
463
|
+
}
|
|
464
|
+
// Metadata store
|
|
465
|
+
if (!db.objectStoreNames.contains('metadata')) {
|
|
466
|
+
db.createObjectStore('metadata', { keyPath: 'key' });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
// Load cached metadata
|
|
471
|
+
await this.loadMetadata();
|
|
472
|
+
return this.db;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get database instance
|
|
476
|
+
*/
|
|
477
|
+
async getDB() {
|
|
478
|
+
if (this.db)
|
|
479
|
+
return this.db;
|
|
480
|
+
if (this.dbPromise)
|
|
481
|
+
return this.dbPromise;
|
|
482
|
+
throw new Error('Database not available');
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Load cached metadata (count, last sync)
|
|
486
|
+
*/
|
|
487
|
+
async loadMetadata() {
|
|
488
|
+
if (!this.db)
|
|
489
|
+
return;
|
|
490
|
+
const countMeta = await this.db.get('metadata', 'documentCount');
|
|
491
|
+
this.documentCount = countMeta?.value || 0;
|
|
492
|
+
const syncMeta = await this.db.get('metadata', 'lastSyncAt');
|
|
493
|
+
this.lastSyncAt = syncMeta?.value || null;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Store documents in cache
|
|
497
|
+
*/
|
|
498
|
+
async store(documents) {
|
|
499
|
+
if (this.config.storage === 'localstorage') {
|
|
500
|
+
return this.storeLocalStorage(documents);
|
|
501
|
+
}
|
|
502
|
+
const db = await this.getDB();
|
|
503
|
+
const tx = db.transaction(['documents', 'metadata'], 'readwrite');
|
|
504
|
+
// Store documents
|
|
505
|
+
for (const doc of documents) {
|
|
506
|
+
await tx.objectStore('documents').put(doc);
|
|
507
|
+
}
|
|
508
|
+
// Update metadata
|
|
509
|
+
this.documentCount = await tx.objectStore('documents').count();
|
|
510
|
+
this.lastSyncAt = new Date().toISOString();
|
|
511
|
+
await tx.objectStore('metadata').put({
|
|
512
|
+
key: 'documentCount',
|
|
513
|
+
value: this.documentCount
|
|
514
|
+
});
|
|
515
|
+
await tx.objectStore('metadata').put({
|
|
516
|
+
key: 'lastSyncAt',
|
|
517
|
+
value: this.lastSyncAt
|
|
518
|
+
});
|
|
519
|
+
await tx.done;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Retrieve all cached documents
|
|
523
|
+
*/
|
|
524
|
+
async retrieve() {
|
|
525
|
+
if (this.config.storage === 'localstorage') {
|
|
526
|
+
return this.retrieveLocalStorage();
|
|
527
|
+
}
|
|
528
|
+
const db = await this.getDB();
|
|
529
|
+
return db.getAll('documents');
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get last sync timestamp
|
|
533
|
+
*/
|
|
534
|
+
async getLastSync() {
|
|
535
|
+
return this.lastSyncAt;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Get cache status
|
|
539
|
+
*/
|
|
540
|
+
getStatus() {
|
|
541
|
+
const now = Date.now();
|
|
542
|
+
const lastSync = this.lastSyncAt ? new Date(this.lastSyncAt).getTime() : 0;
|
|
543
|
+
const isStale = now - lastSync > this.config.maxAge;
|
|
544
|
+
return {
|
|
545
|
+
documentCount: this.documentCount,
|
|
546
|
+
lastSyncAt: this.lastSyncAt,
|
|
547
|
+
cacheSize: this.documentCount * 5000, // Rough estimate: ~5KB per doc
|
|
548
|
+
isStale
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Clear all cached data
|
|
553
|
+
*/
|
|
554
|
+
async clear() {
|
|
555
|
+
if (this.config.storage === 'localstorage') {
|
|
556
|
+
localStorage.removeItem('cybernetic-cache-docs');
|
|
557
|
+
localStorage.removeItem('cybernetic-cache-meta');
|
|
558
|
+
this.documentCount = 0;
|
|
559
|
+
this.lastSyncAt = null;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const db = await this.getDB();
|
|
563
|
+
const tx = db.transaction(['documents', 'metadata'], 'readwrite');
|
|
564
|
+
await tx.objectStore('documents').clear();
|
|
565
|
+
await tx.objectStore('metadata').clear();
|
|
566
|
+
await tx.done;
|
|
567
|
+
this.documentCount = 0;
|
|
568
|
+
this.lastSyncAt = null;
|
|
569
|
+
}
|
|
570
|
+
// ==================== LocalStorage fallback ====================
|
|
571
|
+
storeLocalStorage(documents) {
|
|
572
|
+
try {
|
|
573
|
+
localStorage.setItem('cybernetic-cache-docs', JSON.stringify(documents));
|
|
574
|
+
this.documentCount = documents.length;
|
|
575
|
+
this.lastSyncAt = new Date().toISOString();
|
|
576
|
+
localStorage.setItem('cybernetic-cache-meta', JSON.stringify({
|
|
577
|
+
documentCount: this.documentCount,
|
|
578
|
+
lastSyncAt: this.lastSyncAt
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// LocalStorage full - clear and retry with limit
|
|
583
|
+
localStorage.removeItem('cybernetic-cache-docs');
|
|
584
|
+
const limited = documents.slice(0, 50); // Keep only 50 docs
|
|
585
|
+
localStorage.setItem('cybernetic-cache-docs', JSON.stringify(limited));
|
|
586
|
+
this.documentCount = limited.length;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
retrieveLocalStorage() {
|
|
590
|
+
try {
|
|
591
|
+
const data = localStorage.getItem('cybernetic-cache-docs');
|
|
592
|
+
return data ? JSON.parse(data) : [];
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/CyberneticLocalRAG.ts
|
|
601
|
+
// Local RAG processing for offline fallback
|
|
602
|
+
/**
|
|
603
|
+
* Local RAG engine using TF-IDF similarity
|
|
604
|
+
*
|
|
605
|
+
* Provides offline fallback when backend is unavailable.
|
|
606
|
+
* Uses simple TF-IDF for document matching (no vector embeddings).
|
|
607
|
+
*/
|
|
608
|
+
class CyberneticLocalRAG {
|
|
609
|
+
constructor() {
|
|
610
|
+
this.documents = [];
|
|
611
|
+
this.idf = new Map();
|
|
612
|
+
this.indexed = false;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Check if documents are indexed
|
|
616
|
+
*/
|
|
617
|
+
isIndexed() {
|
|
618
|
+
return this.indexed && this.documents.length > 0;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Index documents for local search
|
|
622
|
+
*/
|
|
623
|
+
async index(documents) {
|
|
624
|
+
this.documents = [];
|
|
625
|
+
this.idf = new Map();
|
|
626
|
+
// Tokenize and build document frequency
|
|
627
|
+
const docFreq = new Map();
|
|
628
|
+
for (const doc of documents) {
|
|
629
|
+
const tokens = this.tokenize(doc.content);
|
|
630
|
+
const uniqueTokens = new Set(tokens);
|
|
631
|
+
// Count document frequency
|
|
632
|
+
for (const token of uniqueTokens) {
|
|
633
|
+
docFreq.set(token, (docFreq.get(token) || 0) + 1);
|
|
634
|
+
}
|
|
635
|
+
// Store indexed document
|
|
636
|
+
this.documents.push({
|
|
637
|
+
id: doc.id,
|
|
638
|
+
title: doc.title,
|
|
639
|
+
content: doc.content,
|
|
640
|
+
tokens,
|
|
641
|
+
tfidf: new Map() // Computed after IDF is known
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
// Compute IDF
|
|
645
|
+
const N = documents.length;
|
|
646
|
+
for (const [term, df] of docFreq) {
|
|
647
|
+
this.idf.set(term, Math.log((N + 1) / (df + 1)) + 1);
|
|
648
|
+
}
|
|
649
|
+
// Compute TF-IDF for each document
|
|
650
|
+
for (const doc of this.documents) {
|
|
651
|
+
const termFreq = this.computeTermFrequency(doc.tokens);
|
|
652
|
+
for (const [term, tf] of termFreq) {
|
|
653
|
+
const idf = this.idf.get(term) || 1;
|
|
654
|
+
doc.tfidf.set(term, tf * idf);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
this.indexed = true;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Process query and generate response
|
|
661
|
+
*/
|
|
662
|
+
async ask(query) {
|
|
663
|
+
if (!this.indexed || this.documents.length === 0) {
|
|
664
|
+
return {
|
|
665
|
+
answer: 'I don\'t have any information available offline.',
|
|
666
|
+
sources: [],
|
|
667
|
+
topScore: 0
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
// Tokenize query and compute TF-IDF
|
|
671
|
+
const queryTokens = this.tokenize(query);
|
|
672
|
+
const queryTf = this.computeTermFrequency(queryTokens);
|
|
673
|
+
const queryTfidf = new Map();
|
|
674
|
+
for (const [term, tf] of queryTf) {
|
|
675
|
+
const idf = this.idf.get(term) || 1;
|
|
676
|
+
queryTfidf.set(term, tf * idf);
|
|
677
|
+
}
|
|
678
|
+
// Compute similarity scores
|
|
679
|
+
const scores = this.documents.map(doc => ({
|
|
680
|
+
doc,
|
|
681
|
+
score: this.cosineSimilarity(queryTfidf, doc.tfidf)
|
|
682
|
+
}));
|
|
683
|
+
// Sort by score descending
|
|
684
|
+
scores.sort((a, b) => b.score - a.score);
|
|
685
|
+
// Take top 3 results
|
|
686
|
+
const topResults = scores.slice(0, 3).filter(r => r.score > 0.1);
|
|
687
|
+
if (topResults.length === 0) {
|
|
688
|
+
return {
|
|
689
|
+
answer: 'I couldn\'t find relevant information for your question in my offline data.',
|
|
690
|
+
sources: [],
|
|
691
|
+
topScore: scores[0]?.score || 0
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
// Generate simple answer from top result
|
|
695
|
+
const topDoc = topResults[0].doc;
|
|
696
|
+
const snippet = this.extractRelevantSnippet(topDoc.content, queryTokens);
|
|
697
|
+
return {
|
|
698
|
+
answer: snippet,
|
|
699
|
+
sources: topResults.map(r => ({
|
|
700
|
+
title: r.doc.title,
|
|
701
|
+
snippet: r.doc.content.substring(0, 200) + '...',
|
|
702
|
+
score: r.score
|
|
703
|
+
})),
|
|
704
|
+
topScore: topResults[0].score
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Reset the index
|
|
709
|
+
*/
|
|
710
|
+
reset() {
|
|
711
|
+
this.documents = [];
|
|
712
|
+
this.idf = new Map();
|
|
713
|
+
this.indexed = false;
|
|
714
|
+
}
|
|
715
|
+
// ==================== PRIVATE METHODS ====================
|
|
716
|
+
/**
|
|
717
|
+
* Tokenize text into words
|
|
718
|
+
*/
|
|
719
|
+
tokenize(text) {
|
|
720
|
+
return text
|
|
721
|
+
.toLowerCase()
|
|
722
|
+
.replace(/[^\w\s]/g, ' ')
|
|
723
|
+
.split(/\s+/)
|
|
724
|
+
.filter(token => token.length > 2 && !this.isStopWord(token));
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Compute term frequency
|
|
728
|
+
*/
|
|
729
|
+
computeTermFrequency(tokens) {
|
|
730
|
+
const freq = new Map();
|
|
731
|
+
for (const token of tokens) {
|
|
732
|
+
freq.set(token, (freq.get(token) || 0) + 1);
|
|
733
|
+
}
|
|
734
|
+
// Normalize by document length
|
|
735
|
+
const maxFreq = Math.max(...freq.values());
|
|
736
|
+
if (maxFreq > 0) {
|
|
737
|
+
for (const [term, f] of freq) {
|
|
738
|
+
freq.set(term, f / maxFreq);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return freq;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Compute cosine similarity between two TF-IDF vectors
|
|
745
|
+
*/
|
|
746
|
+
cosineSimilarity(vec1, vec2) {
|
|
747
|
+
let dotProduct = 0;
|
|
748
|
+
let norm1 = 0;
|
|
749
|
+
let norm2 = 0;
|
|
750
|
+
for (const [term, val1] of vec1) {
|
|
751
|
+
const val2 = vec2.get(term) || 0;
|
|
752
|
+
dotProduct += val1 * val2;
|
|
753
|
+
norm1 += val1 * val1;
|
|
754
|
+
}
|
|
755
|
+
for (const val of vec2.values()) {
|
|
756
|
+
norm2 += val * val;
|
|
757
|
+
}
|
|
758
|
+
if (norm1 === 0 || norm2 === 0)
|
|
759
|
+
return 0;
|
|
760
|
+
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Extract most relevant snippet from content
|
|
764
|
+
*/
|
|
765
|
+
extractRelevantSnippet(content, queryTokens) {
|
|
766
|
+
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
|
767
|
+
// Score sentences by query token overlap
|
|
768
|
+
const scored = sentences.map(sentence => {
|
|
769
|
+
const sentenceTokens = new Set(this.tokenize(sentence));
|
|
770
|
+
let score = 0;
|
|
771
|
+
for (const qt of queryTokens) {
|
|
772
|
+
if (sentenceTokens.has(qt))
|
|
773
|
+
score++;
|
|
774
|
+
}
|
|
775
|
+
return { sentence: sentence.trim(), score };
|
|
776
|
+
});
|
|
777
|
+
scored.sort((a, b) => b.score - a.score);
|
|
778
|
+
// Take top 2-3 sentences
|
|
779
|
+
const topSentences = scored.slice(0, 3).filter(s => s.score > 0);
|
|
780
|
+
if (topSentences.length === 0) {
|
|
781
|
+
return content.substring(0, 300) + '...';
|
|
782
|
+
}
|
|
783
|
+
return topSentences.map(s => s.sentence).join('. ') + '.';
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Check if word is a stop word
|
|
787
|
+
*/
|
|
788
|
+
isStopWord(word) {
|
|
789
|
+
const stopWords = new Set([
|
|
790
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
|
791
|
+
'of', 'with', 'by', 'from', 'up', 'about', 'into', 'through', 'during',
|
|
792
|
+
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
|
|
793
|
+
'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might',
|
|
794
|
+
'this', 'that', 'these', 'those', 'it', 'its', 'they', 'them', 'their',
|
|
795
|
+
'what', 'which', 'who', 'whom', 'when', 'where', 'why', 'how',
|
|
796
|
+
'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other', 'some',
|
|
797
|
+
'such', 'no', 'not', 'only', 'same', 'so', 'than', 'too', 'very'
|
|
798
|
+
]);
|
|
799
|
+
return stopWords.has(word);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/license/base64url.ts
|
|
804
|
+
// Base64URL encoding/decoding utilities for JWT handling
|
|
805
|
+
/**
|
|
806
|
+
* Decode a base64url string to Uint8Array
|
|
807
|
+
* RFC 4648 §5 - Base64 URL-safe encoding
|
|
808
|
+
*/
|
|
809
|
+
function base64urlDecode(input) {
|
|
810
|
+
// Convert base64url to standard base64
|
|
811
|
+
let base64 = input
|
|
812
|
+
.replace(/-/g, '+')
|
|
813
|
+
.replace(/_/g, '/');
|
|
814
|
+
// Add padding if needed
|
|
815
|
+
const padding = base64.length % 4;
|
|
816
|
+
if (padding) {
|
|
817
|
+
base64 += '='.repeat(4 - padding);
|
|
818
|
+
}
|
|
819
|
+
// Decode base64
|
|
820
|
+
const binaryString = atob(base64);
|
|
821
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
822
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
823
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
824
|
+
}
|
|
825
|
+
return bytes;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Decode base64url to UTF-8 string
|
|
829
|
+
*/
|
|
830
|
+
function base64urlDecodeString(input) {
|
|
831
|
+
const bytes = base64urlDecode(input);
|
|
832
|
+
return new TextDecoder().decode(bytes);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Parse a JWT token into its parts (header, payload, signature)
|
|
836
|
+
* Does NOT verify the signature
|
|
837
|
+
*/
|
|
838
|
+
function parseJWT(token) {
|
|
839
|
+
try {
|
|
840
|
+
const parts = token.split('.');
|
|
841
|
+
if (parts.length !== 3) {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
845
|
+
return {
|
|
846
|
+
header: JSON.parse(base64urlDecodeString(headerB64)),
|
|
847
|
+
payload: JSON.parse(base64urlDecodeString(payloadB64)),
|
|
848
|
+
signature: base64urlDecode(signatureB64),
|
|
849
|
+
signedContent: `${headerB64}.${payloadB64}`
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/license/verifier.ts
|
|
858
|
+
// JWT verification for license tokens
|
|
859
|
+
/**
|
|
860
|
+
* AsterMind public key for license verification (ES256 - ECDSA P-256)
|
|
861
|
+
* This key is embedded for verification of official AsterMind licenses
|
|
862
|
+
*/
|
|
863
|
+
const ASTERMIND_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
864
|
+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhLW7MXHQ0Fuk1OKt9D5CbRw7fUMN
|
|
865
|
+
LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
|
|
866
|
+
-----END PUBLIC KEY-----`;
|
|
867
|
+
/**
|
|
868
|
+
* Import a PEM-encoded public key for WebCrypto
|
|
869
|
+
*/
|
|
870
|
+
async function importPublicKey(pemKey) {
|
|
871
|
+
// Remove PEM headers and whitespace
|
|
872
|
+
const pemContents = pemKey
|
|
873
|
+
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
|
874
|
+
.replace(/-----END PUBLIC KEY-----/, '')
|
|
875
|
+
.replace(/\s/g, '');
|
|
876
|
+
// Decode base64 to ArrayBuffer
|
|
877
|
+
const binaryString = atob(pemContents);
|
|
878
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
879
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
880
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
881
|
+
}
|
|
882
|
+
// Import as ECDSA P-256 key
|
|
883
|
+
return crypto.subtle.importKey('spki', bytes.buffer, {
|
|
884
|
+
name: 'ECDSA',
|
|
885
|
+
namedCurve: 'P-256'
|
|
886
|
+
}, false, ['verify']);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Convert JWT signature from compact format to DER format for WebCrypto
|
|
890
|
+
* JWT uses raw R||S format (64 bytes for P-256)
|
|
891
|
+
* WebCrypto expects raw format for ECDSA, not DER
|
|
892
|
+
*/
|
|
893
|
+
function convertJWTSignature(signature) {
|
|
894
|
+
// For P-256, the signature should be 64 bytes (32 for R, 32 for S)
|
|
895
|
+
// JWT compact serialization already uses this format
|
|
896
|
+
return signature;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Verify a JWT signature using WebCrypto
|
|
900
|
+
*/
|
|
901
|
+
async function verifySignature(signedContent, signature, publicKey) {
|
|
902
|
+
try {
|
|
903
|
+
const key = await importPublicKey(publicKey);
|
|
904
|
+
const encoder = new TextEncoder();
|
|
905
|
+
const data = encoder.encode(signedContent);
|
|
906
|
+
const sig = convertJWTSignature(signature);
|
|
907
|
+
// Create a new ArrayBuffer from the signature bytes to satisfy TypeScript
|
|
908
|
+
const sigBuffer = new Uint8Array(sig).buffer;
|
|
909
|
+
return await crypto.subtle.verify({
|
|
910
|
+
name: 'ECDSA',
|
|
911
|
+
hash: 'SHA-256'
|
|
912
|
+
}, key, sigBuffer, data);
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
console.warn('[License] Signature verification failed:', error);
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Validate license payload structure
|
|
921
|
+
*/
|
|
922
|
+
function validatePayload(payload) {
|
|
923
|
+
// Required fields
|
|
924
|
+
if (typeof payload.iss !== 'string')
|
|
925
|
+
return null;
|
|
926
|
+
if (typeof payload.sub !== 'string')
|
|
927
|
+
return null;
|
|
928
|
+
if (typeof payload.aud !== 'string')
|
|
929
|
+
return null;
|
|
930
|
+
if (typeof payload.iat !== 'number')
|
|
931
|
+
return null;
|
|
932
|
+
if (typeof payload.exp !== 'number')
|
|
933
|
+
return null;
|
|
934
|
+
if (typeof payload.plan !== 'string')
|
|
935
|
+
return null;
|
|
936
|
+
if (typeof payload.seats !== 'number')
|
|
937
|
+
return null;
|
|
938
|
+
if (!Array.isArray(payload.features))
|
|
939
|
+
return null;
|
|
940
|
+
if (typeof payload.licenseVersion !== 'number')
|
|
941
|
+
return null;
|
|
942
|
+
return {
|
|
943
|
+
iss: payload.iss,
|
|
944
|
+
sub: payload.sub,
|
|
945
|
+
aud: payload.aud,
|
|
946
|
+
iat: payload.iat,
|
|
947
|
+
exp: payload.exp,
|
|
948
|
+
plan: payload.plan,
|
|
949
|
+
org: typeof payload.org === 'string' ? payload.org : undefined,
|
|
950
|
+
seats: payload.seats,
|
|
951
|
+
features: payload.features,
|
|
952
|
+
graceUntil: typeof payload.graceUntil === 'number' ? payload.graceUntil : undefined,
|
|
953
|
+
licenseVersion: payload.licenseVersion
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Calculate days remaining until expiration
|
|
958
|
+
*/
|
|
959
|
+
function calculateDaysRemaining(expTimestamp) {
|
|
960
|
+
const now = Math.floor(Date.now() / 1000);
|
|
961
|
+
const secondsRemaining = expTimestamp - now;
|
|
962
|
+
return Math.floor(secondsRemaining / 86400); // 86400 = seconds in a day
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Verify a license token
|
|
966
|
+
*
|
|
967
|
+
* @param token - JWT license token
|
|
968
|
+
* @param publicKey - Optional custom public key (defaults to AsterMind key)
|
|
969
|
+
* @returns License state after verification
|
|
970
|
+
*/
|
|
971
|
+
async function verifyLicenseToken(token, publicKey) {
|
|
972
|
+
// No token provided
|
|
973
|
+
if (!token || token.trim() === '') {
|
|
974
|
+
return {
|
|
975
|
+
status: 'missing',
|
|
976
|
+
payload: null,
|
|
977
|
+
error: 'No license key provided',
|
|
978
|
+
inGracePeriod: false,
|
|
979
|
+
daysRemaining: null
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
// Parse JWT
|
|
983
|
+
const parsed = parseJWT(token);
|
|
984
|
+
if (!parsed) {
|
|
985
|
+
return {
|
|
986
|
+
status: 'invalid',
|
|
987
|
+
payload: null,
|
|
988
|
+
error: 'Invalid license key format',
|
|
989
|
+
inGracePeriod: false,
|
|
990
|
+
daysRemaining: null
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
// Validate payload structure
|
|
994
|
+
const payload = validatePayload(parsed.payload);
|
|
995
|
+
if (!payload) {
|
|
996
|
+
return {
|
|
997
|
+
status: 'invalid',
|
|
998
|
+
payload: null,
|
|
999
|
+
error: 'Invalid license payload structure',
|
|
1000
|
+
inGracePeriod: false,
|
|
1001
|
+
daysRemaining: null
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
// Verify issuer
|
|
1005
|
+
if (payload.iss !== 'astermind') {
|
|
1006
|
+
return {
|
|
1007
|
+
status: 'invalid',
|
|
1008
|
+
payload: null,
|
|
1009
|
+
error: 'Invalid license issuer',
|
|
1010
|
+
inGracePeriod: false,
|
|
1011
|
+
daysRemaining: null
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
// Verify audience (should be cybernetic-chatbot or wildcard)
|
|
1015
|
+
if (payload.aud !== 'cybernetic-chatbot' && payload.aud !== '*') {
|
|
1016
|
+
return {
|
|
1017
|
+
status: 'invalid',
|
|
1018
|
+
payload: null,
|
|
1019
|
+
error: 'License not valid for this product',
|
|
1020
|
+
inGracePeriod: false,
|
|
1021
|
+
daysRemaining: null
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
// Verify signature (if WebCrypto is available)
|
|
1025
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
1026
|
+
const keyToUse = publicKey || ASTERMIND_PUBLIC_KEY;
|
|
1027
|
+
const signatureValid = await verifySignature(parsed.signedContent, parsed.signature, keyToUse);
|
|
1028
|
+
if (!signatureValid) {
|
|
1029
|
+
return {
|
|
1030
|
+
status: 'invalid',
|
|
1031
|
+
payload: null,
|
|
1032
|
+
error: 'License signature verification failed',
|
|
1033
|
+
inGracePeriod: false,
|
|
1034
|
+
daysRemaining: null
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// Check expiration
|
|
1039
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1040
|
+
const daysRemaining = calculateDaysRemaining(payload.exp);
|
|
1041
|
+
if (payload.exp < now) {
|
|
1042
|
+
// Check for grace period
|
|
1043
|
+
if (payload.graceUntil && payload.graceUntil > now) {
|
|
1044
|
+
return {
|
|
1045
|
+
status: 'valid',
|
|
1046
|
+
payload,
|
|
1047
|
+
inGracePeriod: true,
|
|
1048
|
+
daysRemaining
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
status: 'expired',
|
|
1053
|
+
payload,
|
|
1054
|
+
error: 'License has expired',
|
|
1055
|
+
inGracePeriod: false,
|
|
1056
|
+
daysRemaining
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
// Check for evaluation license
|
|
1060
|
+
if (payload.plan === 'eval') {
|
|
1061
|
+
return {
|
|
1062
|
+
status: 'eval',
|
|
1063
|
+
payload,
|
|
1064
|
+
inGracePeriod: false,
|
|
1065
|
+
daysRemaining
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
// License is valid
|
|
1069
|
+
return {
|
|
1070
|
+
status: 'valid',
|
|
1071
|
+
payload,
|
|
1072
|
+
inGracePeriod: false,
|
|
1073
|
+
daysRemaining
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Quick check if a token looks like a valid JWT (does not verify)
|
|
1078
|
+
*/
|
|
1079
|
+
function isValidJWTFormat(token) {
|
|
1080
|
+
return parseJWT(token) !== null;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Extract expiration date from a token without verification
|
|
1084
|
+
* Useful for quick checks
|
|
1085
|
+
*/
|
|
1086
|
+
function getTokenExpiration(token) {
|
|
1087
|
+
const parsed = parseJWT(token);
|
|
1088
|
+
if (!parsed || typeof parsed.payload.exp !== 'number') {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
return new Date(parsed.payload.exp * 1000);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/license/licenseManager.ts
|
|
1095
|
+
// License management with environment-aware enforcement
|
|
1096
|
+
/**
|
|
1097
|
+
* Required features for different capabilities
|
|
1098
|
+
*/
|
|
1099
|
+
const REQUIRED_FEATURES = {
|
|
1100
|
+
/** Feature required for basic client functionality */
|
|
1101
|
+
CLIENT: 'cybernetic-chatbot-client',
|
|
1102
|
+
/** Feature required for agentic capabilities (DOM automation, intent classification) */
|
|
1103
|
+
AGENTIC: 'agentic'
|
|
1104
|
+
};
|
|
1105
|
+
/**
|
|
1106
|
+
* License enforcement message shown in production when license is invalid
|
|
1107
|
+
*/
|
|
1108
|
+
const LICENSE_WARNING_MESSAGE = '\n\n---\n⚠️ License Notice: Your AsterMind license key needs to be updated. ' +
|
|
1109
|
+
'Please contact support@astermind.ai or visit https://astermind.ai/license to renew your license.';
|
|
1110
|
+
/**
|
|
1111
|
+
* Detect current environment based on URL and other signals
|
|
1112
|
+
*/
|
|
1113
|
+
function detectEnvironment() {
|
|
1114
|
+
// Node.js environment
|
|
1115
|
+
if (typeof window === 'undefined') {
|
|
1116
|
+
return process?.env?.NODE_ENV === 'production' ? 'production' : 'development';
|
|
1117
|
+
}
|
|
1118
|
+
const hostname = window.location?.hostname || '';
|
|
1119
|
+
// Development indicators
|
|
1120
|
+
const devPatterns = [
|
|
1121
|
+
'localhost',
|
|
1122
|
+
'127.0.0.1',
|
|
1123
|
+
'0.0.0.0',
|
|
1124
|
+
'.local',
|
|
1125
|
+
'.dev',
|
|
1126
|
+
'.test',
|
|
1127
|
+
':3000', // Common dev ports
|
|
1128
|
+
':5173', // Vite
|
|
1129
|
+
':8080', // Common dev port
|
|
1130
|
+
':4200', // Angular
|
|
1131
|
+
];
|
|
1132
|
+
// Check for development patterns
|
|
1133
|
+
for (const pattern of devPatterns) {
|
|
1134
|
+
if (hostname.includes(pattern) || window.location?.port === pattern.replace(':', '')) {
|
|
1135
|
+
return 'development';
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// Check for file:// protocol (local development)
|
|
1139
|
+
if (window.location?.protocol === 'file:') {
|
|
1140
|
+
return 'development';
|
|
1141
|
+
}
|
|
1142
|
+
// Check for staging/preview patterns
|
|
1143
|
+
const stagingPatterns = [
|
|
1144
|
+
'staging.',
|
|
1145
|
+
'preview.',
|
|
1146
|
+
'-preview.',
|
|
1147
|
+
'.vercel.app',
|
|
1148
|
+
'.netlify.app',
|
|
1149
|
+
'.pages.dev',
|
|
1150
|
+
];
|
|
1151
|
+
for (const pattern of stagingPatterns) {
|
|
1152
|
+
if (hostname.includes(pattern)) {
|
|
1153
|
+
// Treat staging as production for license enforcement
|
|
1154
|
+
return 'production';
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
// Default to production for safety
|
|
1158
|
+
return 'production';
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Determine enforcement mode based on environment
|
|
1162
|
+
*/
|
|
1163
|
+
function getEnforcementMode(environment) {
|
|
1164
|
+
return environment === 'development' ? 'soft' : 'hard';
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* License Manager
|
|
1168
|
+
*
|
|
1169
|
+
* Manages license verification and enforcement for CyberneticClient.
|
|
1170
|
+
* - In development: Logs warnings to console (soft enforcement)
|
|
1171
|
+
* - In production: Appends warning to chatbot responses (hard enforcement)
|
|
1172
|
+
*/
|
|
1173
|
+
class LicenseManager {
|
|
1174
|
+
constructor(config = {}) {
|
|
1175
|
+
this.state = null;
|
|
1176
|
+
this.verificationPromise = null;
|
|
1177
|
+
this.hasLoggedWarning = false;
|
|
1178
|
+
this.loggedMissingFeatures = new Set();
|
|
1179
|
+
this.config = config;
|
|
1180
|
+
this.environment = config.environment || detectEnvironment();
|
|
1181
|
+
this.enforcementMode = getEnforcementMode(this.environment);
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Verify the license (async, caches result)
|
|
1185
|
+
*/
|
|
1186
|
+
async verify() {
|
|
1187
|
+
// Return cached state if available
|
|
1188
|
+
if (this.state) {
|
|
1189
|
+
return this.state;
|
|
1190
|
+
}
|
|
1191
|
+
// Prevent concurrent verification calls
|
|
1192
|
+
if (this.verificationPromise) {
|
|
1193
|
+
return this.verificationPromise;
|
|
1194
|
+
}
|
|
1195
|
+
this.verificationPromise = verifyLicenseToken(this.config.licenseKey, this.config.publicKey);
|
|
1196
|
+
try {
|
|
1197
|
+
this.state = await this.verificationPromise;
|
|
1198
|
+
// Notify status change
|
|
1199
|
+
this.config.onStatusChange?.(this.state);
|
|
1200
|
+
// Log based on enforcement mode
|
|
1201
|
+
this.logLicenseStatus();
|
|
1202
|
+
return this.state;
|
|
1203
|
+
}
|
|
1204
|
+
finally {
|
|
1205
|
+
this.verificationPromise = null;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Get current license state (null if not yet verified)
|
|
1210
|
+
*/
|
|
1211
|
+
getState() {
|
|
1212
|
+
return this.state;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Check if license is valid or acceptable
|
|
1216
|
+
* Valid statuses: 'valid', 'eval'
|
|
1217
|
+
*/
|
|
1218
|
+
isValid() {
|
|
1219
|
+
if (!this.state)
|
|
1220
|
+
return false;
|
|
1221
|
+
return this.state.status === 'valid' || this.state.status === 'eval';
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Check if license requires action (expired, missing, invalid)
|
|
1225
|
+
*/
|
|
1226
|
+
requiresAction() {
|
|
1227
|
+
if (!this.state)
|
|
1228
|
+
return true;
|
|
1229
|
+
return this.state.status === 'expired' ||
|
|
1230
|
+
this.state.status === 'missing' ||
|
|
1231
|
+
this.state.status === 'invalid';
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Get current environment
|
|
1235
|
+
*/
|
|
1236
|
+
getEnvironment() {
|
|
1237
|
+
return this.environment;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Get current enforcement mode
|
|
1241
|
+
*/
|
|
1242
|
+
getEnforcementMode() {
|
|
1243
|
+
return this.enforcementMode;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Process a response based on license status
|
|
1247
|
+
* In production with invalid license: appends warning to response
|
|
1248
|
+
* In development: logs warning (once) and returns response unchanged
|
|
1249
|
+
*
|
|
1250
|
+
* @param response - The chatbot response text
|
|
1251
|
+
* @returns Modified response (or original if license valid)
|
|
1252
|
+
*/
|
|
1253
|
+
processResponse(response) {
|
|
1254
|
+
// License not verified yet - allow through
|
|
1255
|
+
if (!this.state) {
|
|
1256
|
+
return response;
|
|
1257
|
+
}
|
|
1258
|
+
// Valid license - no modification
|
|
1259
|
+
if (this.isValid() && !this.state.inGracePeriod) {
|
|
1260
|
+
return response;
|
|
1261
|
+
}
|
|
1262
|
+
// Handle based on enforcement mode
|
|
1263
|
+
if (this.enforcementMode === 'soft') {
|
|
1264
|
+
// Development: just log warning (once)
|
|
1265
|
+
if (!this.hasLoggedWarning) {
|
|
1266
|
+
this.logDevelopmentWarning();
|
|
1267
|
+
this.hasLoggedWarning = true;
|
|
1268
|
+
}
|
|
1269
|
+
return response;
|
|
1270
|
+
}
|
|
1271
|
+
// Production: append warning to response
|
|
1272
|
+
return response + LICENSE_WARNING_MESSAGE;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Get a human-readable license status message
|
|
1276
|
+
*/
|
|
1277
|
+
getStatusMessage() {
|
|
1278
|
+
if (!this.state) {
|
|
1279
|
+
return 'License not verified';
|
|
1280
|
+
}
|
|
1281
|
+
switch (this.state.status) {
|
|
1282
|
+
case 'valid':
|
|
1283
|
+
if (this.state.inGracePeriod) {
|
|
1284
|
+
return `License expired but in grace period (${Math.abs(this.state.daysRemaining || 0)} days overdue)`;
|
|
1285
|
+
}
|
|
1286
|
+
return `License valid (${this.state.daysRemaining} days remaining)`;
|
|
1287
|
+
case 'eval':
|
|
1288
|
+
return `Evaluation license (${this.state.daysRemaining} days remaining)`;
|
|
1289
|
+
case 'expired':
|
|
1290
|
+
return `License expired ${Math.abs(this.state.daysRemaining || 0)} days ago`;
|
|
1291
|
+
case 'missing':
|
|
1292
|
+
return 'No license key provided';
|
|
1293
|
+
case 'invalid':
|
|
1294
|
+
return `Invalid license: ${this.state.error}`;
|
|
1295
|
+
default:
|
|
1296
|
+
return 'Unknown license status';
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Force re-verification (clears cached state)
|
|
1301
|
+
*/
|
|
1302
|
+
async reVerify() {
|
|
1303
|
+
this.state = null;
|
|
1304
|
+
this.hasLoggedWarning = false;
|
|
1305
|
+
this.loggedMissingFeatures.clear();
|
|
1306
|
+
return this.verify();
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Check if a specific feature is licensed
|
|
1310
|
+
*
|
|
1311
|
+
* @param feature - Feature name to check
|
|
1312
|
+
* @returns true if feature is present in license
|
|
1313
|
+
*/
|
|
1314
|
+
hasFeature(feature) {
|
|
1315
|
+
if (!this.state?.payload)
|
|
1316
|
+
return false;
|
|
1317
|
+
return this.state.payload.features.includes(feature);
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Get all licensed features
|
|
1321
|
+
*/
|
|
1322
|
+
getFeatures() {
|
|
1323
|
+
return this.state?.payload?.features || [];
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Check if client feature is licensed
|
|
1327
|
+
* Logs warning if not present (only once)
|
|
1328
|
+
*
|
|
1329
|
+
* @returns true if cybernetic-chatbot-client feature is present
|
|
1330
|
+
*/
|
|
1331
|
+
checkClientFeature() {
|
|
1332
|
+
return this.checkFeatureWithLog(REQUIRED_FEATURES.CLIENT, 'CyberneticClient');
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Check if agentic feature is licensed
|
|
1336
|
+
* Logs warning if not present (only once)
|
|
1337
|
+
* Call this when attempting to use agentic capabilities
|
|
1338
|
+
*
|
|
1339
|
+
* @returns true if agentic feature is present
|
|
1340
|
+
*/
|
|
1341
|
+
checkAgenticFeature() {
|
|
1342
|
+
return this.checkFeatureWithLog(REQUIRED_FEATURES.AGENTIC, 'Agentic capabilities');
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Check a feature and log if missing (only logs once per feature)
|
|
1346
|
+
*/
|
|
1347
|
+
checkFeatureWithLog(feature, displayName) {
|
|
1348
|
+
// If license is missing/invalid, allow through (handled by other checks)
|
|
1349
|
+
if (!this.state?.payload) {
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
const hasFeature = this.state.payload.features.includes(feature);
|
|
1353
|
+
if (!hasFeature && !this.loggedMissingFeatures.has(feature)) {
|
|
1354
|
+
this.loggedMissingFeatures.add(feature);
|
|
1355
|
+
this.logMissingFeature(feature, displayName);
|
|
1356
|
+
}
|
|
1357
|
+
return hasFeature;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Log missing feature warning with available features
|
|
1361
|
+
*/
|
|
1362
|
+
logMissingFeature(missingFeature, displayName) {
|
|
1363
|
+
const availableFeatures = this.getFeatures();
|
|
1364
|
+
const plan = this.state?.payload?.plan || 'unknown';
|
|
1365
|
+
console.warn(`[Cybernetic License] ${displayName} requires the '${missingFeature}' feature, ` +
|
|
1366
|
+
`which is not included in your license.`);
|
|
1367
|
+
console.info(`[Cybernetic License] Current license plan: ${plan}`);
|
|
1368
|
+
console.info(`[Cybernetic License] Licensed features: ${availableFeatures.length > 0
|
|
1369
|
+
? availableFeatures.join(', ')
|
|
1370
|
+
: '(none)'}`);
|
|
1371
|
+
console.info(`[Cybernetic License] To add this feature, upgrade your license at https://astermind.ai/license`);
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Log license status based on enforcement mode
|
|
1375
|
+
*/
|
|
1376
|
+
logLicenseStatus() {
|
|
1377
|
+
if (!this.state)
|
|
1378
|
+
return;
|
|
1379
|
+
const prefix = '[Cybernetic License]';
|
|
1380
|
+
switch (this.state.status) {
|
|
1381
|
+
case 'valid':
|
|
1382
|
+
if (this.state.inGracePeriod) {
|
|
1383
|
+
console.warn(`${prefix} License expired but in grace period. ` +
|
|
1384
|
+
`Please renew at https://astermind.ai/license`);
|
|
1385
|
+
}
|
|
1386
|
+
else if (this.state.daysRemaining !== null && this.state.daysRemaining <= 30) {
|
|
1387
|
+
console.info(`${prefix} License expires in ${this.state.daysRemaining} days`);
|
|
1388
|
+
}
|
|
1389
|
+
break;
|
|
1390
|
+
case 'eval':
|
|
1391
|
+
console.info(`${prefix} Evaluation license - ${this.state.daysRemaining} days remaining`);
|
|
1392
|
+
break;
|
|
1393
|
+
case 'expired':
|
|
1394
|
+
console.error(`${prefix} License expired. ` +
|
|
1395
|
+
`Enforcement mode: ${this.enforcementMode}. ` +
|
|
1396
|
+
`Renew at https://astermind.ai/license`);
|
|
1397
|
+
break;
|
|
1398
|
+
case 'missing':
|
|
1399
|
+
console.warn(`${prefix} No license key provided. ` +
|
|
1400
|
+
`Enforcement mode: ${this.enforcementMode}. ` +
|
|
1401
|
+
`Get a license at https://astermind.ai/license`);
|
|
1402
|
+
break;
|
|
1403
|
+
case 'invalid':
|
|
1404
|
+
console.error(`${prefix} Invalid license: ${this.state.error}. ` +
|
|
1405
|
+
`Enforcement mode: ${this.enforcementMode}`);
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Log development-only warning
|
|
1411
|
+
*/
|
|
1412
|
+
logDevelopmentWarning() {
|
|
1413
|
+
console.warn('[Cybernetic License] Running in development mode with ' +
|
|
1414
|
+
`${this.state?.status || 'unknown'} license. ` +
|
|
1415
|
+
'In production, a warning will be appended to chatbot responses. ' +
|
|
1416
|
+
'Get a license at https://astermind.ai/license');
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Create a pre-configured license manager and verify immediately
|
|
1421
|
+
*/
|
|
1422
|
+
async function createLicenseManager(config) {
|
|
1423
|
+
const manager = new LicenseManager(config);
|
|
1424
|
+
await manager.verify();
|
|
1425
|
+
return manager;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/CyberneticClient.ts
|
|
1429
|
+
// Main client with API calls and offline fallback
|
|
1430
|
+
/**
|
|
1431
|
+
* Cybernetic Chatbot Client
|
|
1432
|
+
*
|
|
1433
|
+
* Provides API access to AsterMind backend with offline fallback.
|
|
1434
|
+
* Always returns a response, never throws exceptions.
|
|
1435
|
+
*/
|
|
1436
|
+
class CyberneticClient {
|
|
1437
|
+
constructor(config) {
|
|
1438
|
+
this.status = 'connecting';
|
|
1439
|
+
this.lastError = null;
|
|
1440
|
+
// Maintenance mode tracking (ADR-200)
|
|
1441
|
+
this.systemSettings = null;
|
|
1442
|
+
this.settingsLastChecked = 0;
|
|
1443
|
+
this.SETTINGS_CHECK_INTERVAL = 300000; // 5 minutes
|
|
1444
|
+
// Agentic capabilities (optional, registered separately via registerAgenticCapabilities)
|
|
1445
|
+
this.agenticCapabilities = null;
|
|
1446
|
+
// Apply defaults
|
|
1447
|
+
this.config = {
|
|
1448
|
+
apiUrl: config.apiUrl,
|
|
1449
|
+
apiKey: config.apiKey,
|
|
1450
|
+
fallback: {
|
|
1451
|
+
enabled: config.fallback?.enabled ?? true,
|
|
1452
|
+
cacheMaxAge: config.fallback?.cacheMaxAge ?? 86400000, // 24 hours
|
|
1453
|
+
cacheOnConnect: config.fallback?.cacheOnConnect ?? true,
|
|
1454
|
+
cacheStorage: config.fallback?.cacheStorage ?? 'indexeddb'
|
|
1455
|
+
},
|
|
1456
|
+
retry: {
|
|
1457
|
+
maxRetries: config.retry?.maxRetries ?? 2,
|
|
1458
|
+
initialDelay: config.retry?.initialDelay ?? 1000,
|
|
1459
|
+
exponentialBackoff: config.retry?.exponentialBackoff ?? true
|
|
1460
|
+
},
|
|
1461
|
+
agentic: config.agentic ?? null,
|
|
1462
|
+
onStatusChange: config.onStatusChange || (() => { }),
|
|
1463
|
+
onError: config.onError || (() => { })
|
|
1464
|
+
};
|
|
1465
|
+
// Initialize components
|
|
1466
|
+
this.apiClient = new ApiClient(this.config.apiUrl, this.config.apiKey);
|
|
1467
|
+
this.cache = new CyberneticCache({
|
|
1468
|
+
storage: this.config.fallback.cacheStorage,
|
|
1469
|
+
maxAge: this.config.fallback.cacheMaxAge
|
|
1470
|
+
});
|
|
1471
|
+
this.localRAG = new CyberneticLocalRAG();
|
|
1472
|
+
// Initialize license manager
|
|
1473
|
+
this.licenseManager = new LicenseManager({
|
|
1474
|
+
licenseKey: config.licenseKey
|
|
1475
|
+
});
|
|
1476
|
+
// Verify license asynchronously (non-blocking)
|
|
1477
|
+
this.licenseManager.verify().then(() => {
|
|
1478
|
+
// Check client feature after verification
|
|
1479
|
+
this.licenseManager.checkClientFeature();
|
|
1480
|
+
}).catch(() => {
|
|
1481
|
+
// License verification errors are handled internally
|
|
1482
|
+
});
|
|
1483
|
+
// Monitor connection status
|
|
1484
|
+
this.monitorConnection();
|
|
1485
|
+
// Pre-cache documents on init if enabled
|
|
1486
|
+
if (this.config.fallback.enabled && this.config.fallback.cacheOnConnect) {
|
|
1487
|
+
this.syncCache().catch(() => {
|
|
1488
|
+
// Silent failure on init - cache may be stale but usable
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
// ==================== AGENTIC CAPABILITIES ====================
|
|
1493
|
+
/**
|
|
1494
|
+
* Register agentic capabilities
|
|
1495
|
+
* Called by registerAgenticCapabilities() helper from agentic module
|
|
1496
|
+
*
|
|
1497
|
+
* @example
|
|
1498
|
+
* ```typescript
|
|
1499
|
+
* import { CyberneticClient, registerAgenticCapabilities } from '@astermind/cybernetic-chatbot-client';
|
|
1500
|
+
* const client = new CyberneticClient(config);
|
|
1501
|
+
* registerAgenticCapabilities(client);
|
|
1502
|
+
* ```
|
|
1503
|
+
*/
|
|
1504
|
+
registerAgentic(capabilities) {
|
|
1505
|
+
this.agenticCapabilities = capabilities;
|
|
1506
|
+
console.log('[Cybernetic] Agentic capabilities registered');
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Check if agentic capabilities are available and enabled
|
|
1510
|
+
*/
|
|
1511
|
+
isAgenticEnabled() {
|
|
1512
|
+
if (this.agenticCapabilities === null || this.config.agentic?.enabled !== true) {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
// Check for agentic feature in license (logs warning if missing)
|
|
1516
|
+
this.licenseManager.checkAgenticFeature();
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Get registered agentic capabilities (for advanced use)
|
|
1521
|
+
*/
|
|
1522
|
+
getAgenticCapabilities() {
|
|
1523
|
+
return this.agenticCapabilities;
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Get agentic configuration
|
|
1527
|
+
*/
|
|
1528
|
+
getAgenticConfig() {
|
|
1529
|
+
return this.config.agentic;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Classify user message intent for agentic action
|
|
1533
|
+
* Only works if agentic capabilities are registered and enabled
|
|
1534
|
+
*
|
|
1535
|
+
* @param message - User's message to classify
|
|
1536
|
+
* @returns Intent classification or null if agentic not available
|
|
1537
|
+
*/
|
|
1538
|
+
classifyIntent(message) {
|
|
1539
|
+
if (!this.isAgenticEnabled() || !this.agenticCapabilities) {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
// Create agent instance if needed for classification
|
|
1543
|
+
const agentConfig = this.config.agentic;
|
|
1544
|
+
const AgentClass = this.agenticCapabilities.agent;
|
|
1545
|
+
const agent = new AgentClass(agentConfig);
|
|
1546
|
+
return agent.interpretIntent(message);
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Execute an agent action
|
|
1550
|
+
* Only works if agentic capabilities are registered and enabled
|
|
1551
|
+
*
|
|
1552
|
+
* @param action - Action to execute
|
|
1553
|
+
* @returns Action result or error
|
|
1554
|
+
*/
|
|
1555
|
+
async executeAction(action) {
|
|
1556
|
+
if (!this.isAgenticEnabled() || !this.agenticCapabilities) {
|
|
1557
|
+
return {
|
|
1558
|
+
success: false,
|
|
1559
|
+
message: 'Agentic capabilities not enabled'
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
const agentConfig = this.config.agentic;
|
|
1563
|
+
const AgentClass = this.agenticCapabilities.agent;
|
|
1564
|
+
const agent = new AgentClass(agentConfig);
|
|
1565
|
+
return agent.executeAction(action);
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Smart ask - checks for action intent first, then falls back to RAG
|
|
1569
|
+
* Combines agentic classification with standard RAG query
|
|
1570
|
+
*
|
|
1571
|
+
* @param message - User's message
|
|
1572
|
+
* @param options - Optional request configuration
|
|
1573
|
+
* @returns Object containing response, action, and/or action result
|
|
1574
|
+
*/
|
|
1575
|
+
async smartAsk(message, options) {
|
|
1576
|
+
// Check for actionable intent if agentic is enabled
|
|
1577
|
+
const classification = this.classifyIntent(message);
|
|
1578
|
+
if (classification && classification.action && !classification.shouldEscalate) {
|
|
1579
|
+
// High confidence action detected
|
|
1580
|
+
if (this.config.agentic?.requireConfirmation !== false) {
|
|
1581
|
+
// Return action for UI confirmation without executing
|
|
1582
|
+
return { action: classification.action };
|
|
1583
|
+
}
|
|
1584
|
+
// Auto-execute if confirmation not required
|
|
1585
|
+
const result = await this.executeAction(classification.action);
|
|
1586
|
+
return {
|
|
1587
|
+
action: classification.action,
|
|
1588
|
+
actionResult: result
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
// Low confidence or no action - proceed to RAG
|
|
1592
|
+
const response = await this.ask(message, options);
|
|
1593
|
+
return { response };
|
|
1594
|
+
}
|
|
1595
|
+
// ==================== CORE METHODS ====================
|
|
1596
|
+
/**
|
|
1597
|
+
* Send a message to the chatbot
|
|
1598
|
+
* Always returns a response, never throws
|
|
1599
|
+
*
|
|
1600
|
+
* @param message - User's message
|
|
1601
|
+
* @param options - Optional request configuration
|
|
1602
|
+
*/
|
|
1603
|
+
async ask(message, options) {
|
|
1604
|
+
// Validate input
|
|
1605
|
+
if (!message || typeof message !== 'string') {
|
|
1606
|
+
return this.createErrorResponse('Message is required', 'none');
|
|
1607
|
+
}
|
|
1608
|
+
// Check maintenance mode before API call (ADR-200)
|
|
1609
|
+
const settings = await this.checkSystemStatus();
|
|
1610
|
+
if (settings.maintenanceMode || settings.forceOfflineClients) {
|
|
1611
|
+
console.log('[Cybernetic] Maintenance mode active, using cached data');
|
|
1612
|
+
// Return maintenance response if no cached data
|
|
1613
|
+
if (!this.isCacheValid()) {
|
|
1614
|
+
return {
|
|
1615
|
+
reply: settings.maintenanceMessage ||
|
|
1616
|
+
'The service is currently under maintenance. Please try again later.',
|
|
1617
|
+
confidence: 'none',
|
|
1618
|
+
sources: [],
|
|
1619
|
+
offline: true,
|
|
1620
|
+
degradedReason: 'Maintenance mode active, no valid cache available'
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
// Use local fallback during maintenance
|
|
1624
|
+
return await this.fallbackAsk(message);
|
|
1625
|
+
}
|
|
1626
|
+
// Try API first
|
|
1627
|
+
try {
|
|
1628
|
+
const response = await this.apiWithRetry(message, options);
|
|
1629
|
+
this.setStatus('online');
|
|
1630
|
+
// Process response through license manager (may add warning in production)
|
|
1631
|
+
const processedReply = this.licenseManager.processResponse(response.reply);
|
|
1632
|
+
return {
|
|
1633
|
+
reply: processedReply,
|
|
1634
|
+
confidence: 'high',
|
|
1635
|
+
sources: response.sources || [],
|
|
1636
|
+
offline: false,
|
|
1637
|
+
sessionId: response.sessionId
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
catch (error) {
|
|
1641
|
+
const omegaError = this.normalizeError(error);
|
|
1642
|
+
this.lastError = omegaError;
|
|
1643
|
+
this.config.onError(omegaError);
|
|
1644
|
+
// Rate limit - don't fallback, return error response
|
|
1645
|
+
if (omegaError.code === 'RATE_LIMIT') {
|
|
1646
|
+
return {
|
|
1647
|
+
reply: 'I\'m receiving too many requests right now. Please try again in a moment.',
|
|
1648
|
+
confidence: 'none',
|
|
1649
|
+
sources: [],
|
|
1650
|
+
offline: false,
|
|
1651
|
+
retryAfter: omegaError.retryAfter
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
// Try fallback if enabled and not explicitly skipped
|
|
1655
|
+
if (this.config.fallback.enabled && !options?.skipFallback) {
|
|
1656
|
+
return await this.fallbackAsk(message);
|
|
1657
|
+
}
|
|
1658
|
+
// No fallback - return error response
|
|
1659
|
+
return this.createErrorResponse('Unable to connect to the chatbot service. Please try again later.', 'none', omegaError.retryAfter);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Send a message with streaming response
|
|
1664
|
+
*
|
|
1665
|
+
* @param message - User's message
|
|
1666
|
+
* @param callbacks - Streaming event callbacks
|
|
1667
|
+
* @param options - Optional request configuration
|
|
1668
|
+
*/
|
|
1669
|
+
async askStream(message, callbacks, options) {
|
|
1670
|
+
if (!message || typeof message !== 'string') {
|
|
1671
|
+
callbacks.onError?.({
|
|
1672
|
+
code: 'LOCAL_RAG_ERROR',
|
|
1673
|
+
message: 'Message is required'
|
|
1674
|
+
});
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
// Check maintenance mode before API call (ADR-200)
|
|
1678
|
+
const settings = await this.checkSystemStatus();
|
|
1679
|
+
if (settings.maintenanceMode || settings.forceOfflineClients) {
|
|
1680
|
+
console.log('[Cybernetic] Maintenance mode active, falling back to offline');
|
|
1681
|
+
const response = await this.fallbackAsk(message);
|
|
1682
|
+
callbacks.onComplete?.(response);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
try {
|
|
1686
|
+
await this.apiClient.chatStream(message, {
|
|
1687
|
+
sessionId: options?.sessionId,
|
|
1688
|
+
context: options?.context,
|
|
1689
|
+
onToken: callbacks.onToken,
|
|
1690
|
+
onSources: callbacks.onSources,
|
|
1691
|
+
onComplete: (data) => {
|
|
1692
|
+
this.setStatus('online');
|
|
1693
|
+
// Process response through license manager (may add warning in production)
|
|
1694
|
+
const processedReply = this.licenseManager.processResponse(data.fullText);
|
|
1695
|
+
callbacks.onComplete?.({
|
|
1696
|
+
reply: processedReply,
|
|
1697
|
+
confidence: 'high',
|
|
1698
|
+
sources: data.sources || [],
|
|
1699
|
+
offline: false,
|
|
1700
|
+
sessionId: data.sessionId
|
|
1701
|
+
});
|
|
1702
|
+
},
|
|
1703
|
+
onError: (error) => {
|
|
1704
|
+
const omegaError = this.normalizeError(error);
|
|
1705
|
+
this.config.onError(omegaError);
|
|
1706
|
+
callbacks.onError?.(omegaError);
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
catch (error) {
|
|
1711
|
+
const omegaError = this.normalizeError(error);
|
|
1712
|
+
this.lastError = omegaError;
|
|
1713
|
+
this.config.onError(omegaError);
|
|
1714
|
+
// Fall back to non-streaming if enabled
|
|
1715
|
+
if (this.config.fallback.enabled && !options?.skipFallback) {
|
|
1716
|
+
const response = await this.fallbackAsk(message);
|
|
1717
|
+
callbacks.onComplete?.(response);
|
|
1718
|
+
}
|
|
1719
|
+
else {
|
|
1720
|
+
callbacks.onError?.(omegaError);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Sync documents to local cache for offline use
|
|
1726
|
+
*/
|
|
1727
|
+
async syncCache() {
|
|
1728
|
+
try {
|
|
1729
|
+
const lastSync = await this.cache.getLastSync();
|
|
1730
|
+
const docs = await this.apiClient.getGeneralDocs(lastSync);
|
|
1731
|
+
if (docs.length > 0) {
|
|
1732
|
+
await this.cache.store(docs);
|
|
1733
|
+
await this.localRAG.index(docs);
|
|
1734
|
+
console.log(`[Cybernetic] Cache synced: ${docs.length} documents`);
|
|
1735
|
+
}
|
|
1736
|
+
this.setStatus('online');
|
|
1737
|
+
}
|
|
1738
|
+
catch (error) {
|
|
1739
|
+
console.warn('[Cybernetic] Cache sync failed:', error);
|
|
1740
|
+
// Don't change status - we may still have cached data
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Get current connection status including system settings
|
|
1745
|
+
*/
|
|
1746
|
+
getStatus() {
|
|
1747
|
+
return {
|
|
1748
|
+
connection: this.status,
|
|
1749
|
+
cache: this.cache.getStatus(),
|
|
1750
|
+
lastError: this.lastError,
|
|
1751
|
+
systemSettings: this.systemSettings,
|
|
1752
|
+
license: this.licenseManager.getState()
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Get license manager for advanced license operations
|
|
1757
|
+
*/
|
|
1758
|
+
getLicenseManager() {
|
|
1759
|
+
return this.licenseManager;
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Clear local cache
|
|
1763
|
+
*/
|
|
1764
|
+
async clearCache() {
|
|
1765
|
+
await this.cache.clear();
|
|
1766
|
+
this.localRAG.reset();
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Manually check if backend is reachable
|
|
1770
|
+
*/
|
|
1771
|
+
async checkConnection() {
|
|
1772
|
+
try {
|
|
1773
|
+
await this.apiClient.getStatus();
|
|
1774
|
+
this.setStatus('online');
|
|
1775
|
+
return true;
|
|
1776
|
+
}
|
|
1777
|
+
catch {
|
|
1778
|
+
this.setStatus('offline');
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
// ==================== MAINTENANCE MODE METHODS (ADR-200) ====================
|
|
1783
|
+
/**
|
|
1784
|
+
* Check system status including maintenance mode
|
|
1785
|
+
* Called periodically and before API calls
|
|
1786
|
+
*/
|
|
1787
|
+
async checkSystemStatus() {
|
|
1788
|
+
// Use cached settings if recently checked
|
|
1789
|
+
if (this.systemSettings &&
|
|
1790
|
+
Date.now() - this.settingsLastChecked < this.SETTINGS_CHECK_INTERVAL) {
|
|
1791
|
+
return this.systemSettings;
|
|
1792
|
+
}
|
|
1793
|
+
try {
|
|
1794
|
+
const status = await this.apiClient.getStatus();
|
|
1795
|
+
// Extract system settings from status response
|
|
1796
|
+
this.systemSettings = {
|
|
1797
|
+
cacheRetentionHours: status.systemSettings?.cacheRetentionHours ?? 168,
|
|
1798
|
+
maintenanceMode: status.systemSettings?.maintenanceMode ?? false,
|
|
1799
|
+
maintenanceMessage: status.systemSettings?.maintenanceMessage,
|
|
1800
|
+
forceOfflineClients: status.systemSettings?.forceOfflineClients ?? false
|
|
1801
|
+
};
|
|
1802
|
+
this.settingsLastChecked = Date.now();
|
|
1803
|
+
return this.systemSettings;
|
|
1804
|
+
}
|
|
1805
|
+
catch {
|
|
1806
|
+
// On error, return cached settings or safe defaults
|
|
1807
|
+
return this.systemSettings || {
|
|
1808
|
+
cacheRetentionHours: 168,
|
|
1809
|
+
maintenanceMode: false,
|
|
1810
|
+
forceOfflineClients: false
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Check if maintenance mode is active
|
|
1816
|
+
*/
|
|
1817
|
+
isMaintenanceMode() {
|
|
1818
|
+
return this.systemSettings?.maintenanceMode ?? false;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Get maintenance message if in maintenance mode
|
|
1822
|
+
*/
|
|
1823
|
+
getMaintenanceMessage() {
|
|
1824
|
+
return this.systemSettings?.maintenanceMessage;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Validate cache using server-configured retention hours
|
|
1828
|
+
*/
|
|
1829
|
+
isCacheValid() {
|
|
1830
|
+
const cacheStatus = this.cache.getStatus();
|
|
1831
|
+
if (!cacheStatus.lastSyncAt)
|
|
1832
|
+
return false;
|
|
1833
|
+
// Use server-configured retention or default 168 hours (7 days)
|
|
1834
|
+
const retentionHours = this.systemSettings?.cacheRetentionHours || 168;
|
|
1835
|
+
const retentionMs = retentionHours * 60 * 60 * 1000;
|
|
1836
|
+
const lastSyncTime = new Date(cacheStatus.lastSyncAt).getTime();
|
|
1837
|
+
return Date.now() - lastSyncTime < retentionMs;
|
|
1838
|
+
}
|
|
1839
|
+
// ==================== PRIVATE METHODS ====================
|
|
1840
|
+
/**
|
|
1841
|
+
* API call with retry logic
|
|
1842
|
+
*/
|
|
1843
|
+
async apiWithRetry(message, options) {
|
|
1844
|
+
const { maxRetries, initialDelay, exponentialBackoff } = this.config.retry;
|
|
1845
|
+
let lastError = null;
|
|
1846
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1847
|
+
try {
|
|
1848
|
+
return await this.apiClient.chat(message, {
|
|
1849
|
+
sessionId: options?.sessionId,
|
|
1850
|
+
context: options?.context
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
catch (error) {
|
|
1854
|
+
lastError = error;
|
|
1855
|
+
// Don't retry on auth or rate limit errors
|
|
1856
|
+
const omegaError = this.normalizeError(error);
|
|
1857
|
+
if (omegaError.code === 'AUTH_ERROR' || omegaError.code === 'RATE_LIMIT') {
|
|
1858
|
+
throw error;
|
|
1859
|
+
}
|
|
1860
|
+
// Wait before retry
|
|
1861
|
+
if (attempt < maxRetries) {
|
|
1862
|
+
const delay = exponentialBackoff
|
|
1863
|
+
? initialDelay * Math.pow(2, attempt)
|
|
1864
|
+
: initialDelay;
|
|
1865
|
+
await this.sleep(delay);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
throw lastError;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Fallback to local RAG processing
|
|
1873
|
+
*/
|
|
1874
|
+
async fallbackAsk(message) {
|
|
1875
|
+
this.setStatus('offline');
|
|
1876
|
+
// Check if we have cached documents
|
|
1877
|
+
const cacheStatus = this.cache.getStatus();
|
|
1878
|
+
if (cacheStatus.documentCount === 0) {
|
|
1879
|
+
return {
|
|
1880
|
+
reply: 'I\'m currently offline and don\'t have any cached information. Please check your connection and try again.',
|
|
1881
|
+
confidence: 'none',
|
|
1882
|
+
sources: [],
|
|
1883
|
+
offline: true,
|
|
1884
|
+
degradedReason: 'No cached documents available'
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
try {
|
|
1888
|
+
// Get documents from cache
|
|
1889
|
+
const docs = await this.cache.retrieve();
|
|
1890
|
+
// Ensure local RAG is indexed
|
|
1891
|
+
if (!this.localRAG.isIndexed()) {
|
|
1892
|
+
await this.localRAG.index(docs);
|
|
1893
|
+
}
|
|
1894
|
+
// Process with local RAG
|
|
1895
|
+
const result = await this.localRAG.ask(message);
|
|
1896
|
+
// Determine confidence based on match quality
|
|
1897
|
+
let confidence = 'medium';
|
|
1898
|
+
if (result.topScore < 0.3) {
|
|
1899
|
+
confidence = 'low';
|
|
1900
|
+
}
|
|
1901
|
+
else if (result.topScore > 0.7) {
|
|
1902
|
+
confidence = 'medium'; // Never 'high' for offline
|
|
1903
|
+
}
|
|
1904
|
+
// Process response through license manager (may add warning in production)
|
|
1905
|
+
const processedReply = this.licenseManager.processResponse(result.answer);
|
|
1906
|
+
return {
|
|
1907
|
+
reply: processedReply,
|
|
1908
|
+
confidence,
|
|
1909
|
+
sources: result.sources.map(s => ({
|
|
1910
|
+
title: s.title,
|
|
1911
|
+
snippet: s.snippet,
|
|
1912
|
+
relevance: s.score
|
|
1913
|
+
})),
|
|
1914
|
+
offline: true,
|
|
1915
|
+
degradedReason: cacheStatus.isStale
|
|
1916
|
+
? 'Using stale cached data'
|
|
1917
|
+
: 'Processed locally without server'
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
catch (error) {
|
|
1921
|
+
console.error('[Cybernetic] Local RAG error:', error);
|
|
1922
|
+
return {
|
|
1923
|
+
reply: 'I\'m having trouble processing your request offline. Please try again when you\'re back online.',
|
|
1924
|
+
confidence: 'none',
|
|
1925
|
+
sources: [],
|
|
1926
|
+
offline: true,
|
|
1927
|
+
degradedReason: 'Local RAG processing failed'
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Monitor browser online/offline events
|
|
1933
|
+
*/
|
|
1934
|
+
monitorConnection() {
|
|
1935
|
+
if (typeof window !== 'undefined') {
|
|
1936
|
+
window.addEventListener('online', () => {
|
|
1937
|
+
this.syncCache().catch(() => { });
|
|
1938
|
+
});
|
|
1939
|
+
window.addEventListener('offline', () => {
|
|
1940
|
+
this.setStatus('offline');
|
|
1941
|
+
});
|
|
1942
|
+
// Set initial status
|
|
1943
|
+
if (!navigator.onLine) {
|
|
1944
|
+
this.setStatus('offline');
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Update connection status and notify listeners
|
|
1950
|
+
*/
|
|
1951
|
+
setStatus(status) {
|
|
1952
|
+
if (this.status !== status) {
|
|
1953
|
+
this.status = status;
|
|
1954
|
+
this.config.onStatusChange(status);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Normalize various error types to CyberneticError
|
|
1959
|
+
*/
|
|
1960
|
+
normalizeError(error) {
|
|
1961
|
+
if (error instanceof Error) {
|
|
1962
|
+
// Check for specific error types
|
|
1963
|
+
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
|
|
1964
|
+
return {
|
|
1965
|
+
code: 'AUTH_ERROR',
|
|
1966
|
+
message: 'Invalid or expired API key',
|
|
1967
|
+
originalError: error
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
if (error.message.includes('429') || error.message.includes('Rate limit')) {
|
|
1971
|
+
const match = error.message.match(/retry after (\d+)/i);
|
|
1972
|
+
return {
|
|
1973
|
+
code: 'RATE_LIMIT',
|
|
1974
|
+
message: 'Rate limit exceeded',
|
|
1975
|
+
retryAfter: match ? parseInt(match[1], 10) : 60,
|
|
1976
|
+
originalError: error
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
if (error.message.includes('5') || error.message.includes('Server')) {
|
|
1980
|
+
return {
|
|
1981
|
+
code: 'SERVER_ERROR',
|
|
1982
|
+
message: 'Server error occurred',
|
|
1983
|
+
originalError: error
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
code: 'NETWORK_ERROR',
|
|
1988
|
+
message: error.message || 'Network error',
|
|
1989
|
+
originalError: error
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
return {
|
|
1993
|
+
code: 'NETWORK_ERROR',
|
|
1994
|
+
message: 'Unknown error occurred'
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Create standardized error response
|
|
1999
|
+
*/
|
|
2000
|
+
createErrorResponse(message, confidence, retryAfter) {
|
|
2001
|
+
return {
|
|
2002
|
+
reply: message,
|
|
2003
|
+
confidence,
|
|
2004
|
+
sources: [],
|
|
2005
|
+
offline: false,
|
|
2006
|
+
retryAfter
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Async sleep utility
|
|
2011
|
+
*/
|
|
2012
|
+
sleep(ms) {
|
|
2013
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/config.ts
|
|
2018
|
+
// Configuration loading and validation
|
|
2019
|
+
/**
|
|
2020
|
+
* Validate configuration
|
|
2021
|
+
*/
|
|
2022
|
+
function validateConfig(config) {
|
|
2023
|
+
if (!config || typeof config !== 'object') {
|
|
2024
|
+
throw new Error('Config must be an object');
|
|
2025
|
+
}
|
|
2026
|
+
const c = config;
|
|
2027
|
+
if (!c.apiUrl || typeof c.apiUrl !== 'string') {
|
|
2028
|
+
throw new Error('apiUrl is required and must be a string');
|
|
2029
|
+
}
|
|
2030
|
+
if (!c.apiKey || typeof c.apiKey !== 'string') {
|
|
2031
|
+
throw new Error('apiKey is required and must be a string');
|
|
2032
|
+
}
|
|
2033
|
+
if (!c.apiKey.startsWith('am_')) {
|
|
2034
|
+
throw new Error('apiKey must start with "am_"');
|
|
2035
|
+
}
|
|
2036
|
+
return true;
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Load config from window.astermindConfig or script tag data attributes
|
|
2040
|
+
*/
|
|
2041
|
+
function loadConfig() {
|
|
2042
|
+
if (typeof window === 'undefined') {
|
|
2043
|
+
return null;
|
|
2044
|
+
}
|
|
2045
|
+
// Check for global config object
|
|
2046
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2047
|
+
if (window.astermindConfig) {
|
|
2048
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2049
|
+
const config = window.astermindConfig;
|
|
2050
|
+
validateConfig(config);
|
|
2051
|
+
return config;
|
|
2052
|
+
}
|
|
2053
|
+
// Check for script tag with data attributes
|
|
2054
|
+
const script = document.querySelector('script[data-astermind-key]');
|
|
2055
|
+
if (script) {
|
|
2056
|
+
return {
|
|
2057
|
+
apiUrl: script.getAttribute('data-astermind-url') || 'https://api.astermind.ai',
|
|
2058
|
+
apiKey: script.getAttribute('data-astermind-key') || ''
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// src/agentic/CyberneticIntentClassifier.ts
|
|
2065
|
+
// Hybrid intent classification for agentic capabilities
|
|
2066
|
+
/**
|
|
2067
|
+
* Action intent patterns with weights
|
|
2068
|
+
*/
|
|
2069
|
+
const ACTION_PATTERNS = {
|
|
2070
|
+
navigate: [
|
|
2071
|
+
{ pattern: /^(go to|navigate to|take me to|open|show me|bring up)\s+(.+)/i, type: 'navigate', weight: 0.9 },
|
|
2072
|
+
{ pattern: /^(i want to see|i need to go to|let me see)\s+(.+)/i, type: 'navigate', weight: 0.85 },
|
|
2073
|
+
{ pattern: /^(where is|how do i get to)\s+(.+)/i, type: 'navigate', weight: 0.6 }, // Lower - might be question
|
|
2074
|
+
],
|
|
2075
|
+
fillForm: [
|
|
2076
|
+
{ pattern: /^(search for|find|look up|search)\s+(.+)/i, type: 'fillForm', weight: 0.88 },
|
|
2077
|
+
{ pattern: /^(filter by|set .+ to|change .+ to)\s+(.+)/i, type: 'fillForm', weight: 0.85 },
|
|
2078
|
+
{ pattern: /^(enter|type|put)\s+(.+)\s+(in|into)\s+(.+)/i, type: 'fillForm', weight: 0.9 },
|
|
2079
|
+
],
|
|
2080
|
+
clickElement: [
|
|
2081
|
+
{ pattern: /^(click|press|hit|tap)\s+(the\s+)?(.+)\s+(button|link)/i, type: 'clickElement', weight: 0.9 },
|
|
2082
|
+
{ pattern: /^(submit|send|confirm|save|cancel|delete)/i, type: 'clickElement', weight: 0.85 },
|
|
2083
|
+
],
|
|
2084
|
+
triggerModal: [
|
|
2085
|
+
{ pattern: /^(open|show|display)\s+(the\s+)?(.+)\s+(modal|dialog|popup|window)/i, type: 'triggerModal', weight: 0.9 },
|
|
2086
|
+
{ pattern: /^(open|show)\s+(help|settings|preferences|options)/i, type: 'triggerModal', weight: 0.85 },
|
|
2087
|
+
],
|
|
2088
|
+
scroll: [
|
|
2089
|
+
{ pattern: /^(scroll to|jump to|go to)\s+(the\s+)?(.+)\s+(section|area|part)/i, type: 'scroll', weight: 0.85 },
|
|
2090
|
+
{ pattern: /^(show me|take me to)\s+(the\s+)?(bottom|top|end|beginning)/i, type: 'scroll', weight: 0.8 },
|
|
2091
|
+
],
|
|
2092
|
+
highlight: [
|
|
2093
|
+
{ pattern: /^(highlight|show me|point to|where is)\s+(the\s+)?(.+)/i, type: 'highlight', weight: 0.7 },
|
|
2094
|
+
],
|
|
2095
|
+
custom: [
|
|
2096
|
+
{ pattern: /^(export|download|refresh|reload|sync)/i, type: 'custom', weight: 0.85 },
|
|
2097
|
+
]
|
|
2098
|
+
};
|
|
2099
|
+
/**
|
|
2100
|
+
* Intent classifier for agentic actions
|
|
2101
|
+
*
|
|
2102
|
+
* Classifies user messages to determine if they should trigger
|
|
2103
|
+
* a local DOM action or be sent to the backend RAG.
|
|
2104
|
+
*/
|
|
2105
|
+
class CyberneticIntentClassifier {
|
|
2106
|
+
constructor(config) {
|
|
2107
|
+
this.config = config;
|
|
2108
|
+
this.siteMapIndex = new Map();
|
|
2109
|
+
this.formIndex = new Map();
|
|
2110
|
+
this.modalIndex = new Map();
|
|
2111
|
+
// Build indexes for fast lookup
|
|
2112
|
+
this.buildIndexes();
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Build search indexes from configuration
|
|
2116
|
+
*/
|
|
2117
|
+
buildIndexes() {
|
|
2118
|
+
// Site map index
|
|
2119
|
+
if (this.config.siteMap) {
|
|
2120
|
+
for (const entry of this.config.siteMap) {
|
|
2121
|
+
// Index by name and aliases
|
|
2122
|
+
const keys = [
|
|
2123
|
+
entry.name.toLowerCase(),
|
|
2124
|
+
entry.path.toLowerCase(),
|
|
2125
|
+
...(entry.aliases || []).map(a => a.toLowerCase())
|
|
2126
|
+
];
|
|
2127
|
+
for (const key of keys) {
|
|
2128
|
+
this.siteMapIndex.set(key, entry);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
// Form fields index
|
|
2133
|
+
if (this.config.forms) {
|
|
2134
|
+
for (const [key, field] of Object.entries(this.config.forms)) {
|
|
2135
|
+
const keys = [
|
|
2136
|
+
key.toLowerCase(),
|
|
2137
|
+
field.name.toLowerCase(),
|
|
2138
|
+
...(field.aliases || []).map(a => a.toLowerCase())
|
|
2139
|
+
];
|
|
2140
|
+
for (const k of keys) {
|
|
2141
|
+
this.formIndex.set(k, field);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
// Modal triggers index
|
|
2146
|
+
if (this.config.modals) {
|
|
2147
|
+
for (const [key, modal] of Object.entries(this.config.modals)) {
|
|
2148
|
+
const keys = [
|
|
2149
|
+
key.toLowerCase(),
|
|
2150
|
+
modal.id.toLowerCase(),
|
|
2151
|
+
...(modal.aliases || []).map(a => a.toLowerCase())
|
|
2152
|
+
];
|
|
2153
|
+
for (const k of keys) {
|
|
2154
|
+
this.modalIndex.set(k, modal);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Classify user message intent
|
|
2161
|
+
*
|
|
2162
|
+
* @param message - User's message to classify
|
|
2163
|
+
* @returns Classification result with action and confidence
|
|
2164
|
+
*/
|
|
2165
|
+
classify(message) {
|
|
2166
|
+
if (!this.config.enabled) {
|
|
2167
|
+
return {
|
|
2168
|
+
action: null,
|
|
2169
|
+
shouldEscalate: true,
|
|
2170
|
+
rawConfidence: 0
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
const normalizedMessage = message.trim().toLowerCase();
|
|
2174
|
+
// Try each action type
|
|
2175
|
+
for (const [actionType, patterns] of Object.entries(ACTION_PATTERNS)) {
|
|
2176
|
+
for (const { pattern, weight } of patterns) {
|
|
2177
|
+
const match = message.match(pattern);
|
|
2178
|
+
if (match) {
|
|
2179
|
+
const result = this.buildAction(actionType, match, weight);
|
|
2180
|
+
if (result) {
|
|
2181
|
+
return result;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
// No pattern match - try fuzzy site map match
|
|
2187
|
+
const siteMapResult = this.fuzzyMatchSiteMap(normalizedMessage);
|
|
2188
|
+
if (siteMapResult) {
|
|
2189
|
+
return siteMapResult;
|
|
2190
|
+
}
|
|
2191
|
+
// Check for custom action keywords
|
|
2192
|
+
const customResult = this.matchCustomAction(normalizedMessage);
|
|
2193
|
+
if (customResult) {
|
|
2194
|
+
return customResult;
|
|
2195
|
+
}
|
|
2196
|
+
// No match - escalate to backend
|
|
2197
|
+
return {
|
|
2198
|
+
action: null,
|
|
2199
|
+
shouldEscalate: true,
|
|
2200
|
+
rawConfidence: 0,
|
|
2201
|
+
matchedPatterns: []
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Build action from pattern match
|
|
2206
|
+
*/
|
|
2207
|
+
buildAction(actionType, match, baseWeight) {
|
|
2208
|
+
const threshold = this.config.confidenceThreshold ?? 0.8;
|
|
2209
|
+
switch (actionType) {
|
|
2210
|
+
case 'navigate': {
|
|
2211
|
+
const target = this.extractTarget(match);
|
|
2212
|
+
const siteMatch = this.findSiteMapMatch(target);
|
|
2213
|
+
if (siteMatch) {
|
|
2214
|
+
const confidence = baseWeight * siteMatch.similarity;
|
|
2215
|
+
return {
|
|
2216
|
+
action: {
|
|
2217
|
+
id: `action-${Date.now()}`,
|
|
2218
|
+
type: 'navigate',
|
|
2219
|
+
target: siteMatch.entry.path,
|
|
2220
|
+
confidence,
|
|
2221
|
+
explanation: `Navigate to ${siteMatch.entry.name}`
|
|
2222
|
+
},
|
|
2223
|
+
shouldEscalate: confidence < threshold,
|
|
2224
|
+
rawConfidence: confidence,
|
|
2225
|
+
matchedPatterns: ['navigate', siteMatch.entry.name]
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
break;
|
|
2229
|
+
}
|
|
2230
|
+
case 'fillForm': {
|
|
2231
|
+
const target = this.extractTarget(match);
|
|
2232
|
+
const { field, value } = this.parseFormIntent(match);
|
|
2233
|
+
if (field) {
|
|
2234
|
+
const confidence = baseWeight * 0.95;
|
|
2235
|
+
return {
|
|
2236
|
+
action: {
|
|
2237
|
+
id: `action-${Date.now()}`,
|
|
2238
|
+
type: 'fillForm',
|
|
2239
|
+
target: field.selector,
|
|
2240
|
+
params: { value },
|
|
2241
|
+
confidence,
|
|
2242
|
+
explanation: `Fill "${field.name}" with "${value}"`
|
|
2243
|
+
},
|
|
2244
|
+
shouldEscalate: confidence < threshold,
|
|
2245
|
+
rawConfidence: confidence,
|
|
2246
|
+
matchedPatterns: ['fillForm', field.name]
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
// Default to search field
|
|
2250
|
+
if (this.config.forms?.search) {
|
|
2251
|
+
const confidence = baseWeight * 0.9;
|
|
2252
|
+
return {
|
|
2253
|
+
action: {
|
|
2254
|
+
id: `action-${Date.now()}`,
|
|
2255
|
+
type: 'fillForm',
|
|
2256
|
+
target: this.config.forms.search.selector,
|
|
2257
|
+
params: { value: target },
|
|
2258
|
+
confidence,
|
|
2259
|
+
explanation: `Search for "${target}"`
|
|
2260
|
+
},
|
|
2261
|
+
shouldEscalate: confidence < threshold,
|
|
2262
|
+
rawConfidence: confidence,
|
|
2263
|
+
matchedPatterns: ['fillForm', 'search']
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
case 'clickElement': {
|
|
2269
|
+
const buttonName = this.extractTarget(match);
|
|
2270
|
+
const selector = this.findButtonSelector(buttonName);
|
|
2271
|
+
if (selector) {
|
|
2272
|
+
const confidence = baseWeight * 0.95;
|
|
2273
|
+
return {
|
|
2274
|
+
action: {
|
|
2275
|
+
id: `action-${Date.now()}`,
|
|
2276
|
+
type: 'clickElement',
|
|
2277
|
+
target: selector,
|
|
2278
|
+
confidence,
|
|
2279
|
+
explanation: `Click "${buttonName}" button`
|
|
2280
|
+
},
|
|
2281
|
+
shouldEscalate: confidence < threshold,
|
|
2282
|
+
rawConfidence: confidence,
|
|
2283
|
+
matchedPatterns: ['clickElement', buttonName]
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
break;
|
|
2287
|
+
}
|
|
2288
|
+
case 'triggerModal': {
|
|
2289
|
+
const modalName = this.extractTarget(match);
|
|
2290
|
+
const modal = this.findModalConfig(modalName);
|
|
2291
|
+
if (modal) {
|
|
2292
|
+
const confidence = baseWeight * 0.95;
|
|
2293
|
+
return {
|
|
2294
|
+
action: {
|
|
2295
|
+
id: `action-${Date.now()}`,
|
|
2296
|
+
type: 'triggerModal',
|
|
2297
|
+
target: modal.trigger,
|
|
2298
|
+
confidence,
|
|
2299
|
+
explanation: `Open ${modal.id} modal`
|
|
2300
|
+
},
|
|
2301
|
+
shouldEscalate: confidence < threshold,
|
|
2302
|
+
rawConfidence: confidence,
|
|
2303
|
+
matchedPatterns: ['triggerModal', modal.id]
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
break;
|
|
2307
|
+
}
|
|
2308
|
+
case 'scroll': {
|
|
2309
|
+
const target = this.extractTarget(match);
|
|
2310
|
+
const selector = this.findScrollTarget(target);
|
|
2311
|
+
if (selector) {
|
|
2312
|
+
const confidence = baseWeight * 0.9;
|
|
2313
|
+
return {
|
|
2314
|
+
action: {
|
|
2315
|
+
id: `action-${Date.now()}`,
|
|
2316
|
+
type: 'scroll',
|
|
2317
|
+
target: selector,
|
|
2318
|
+
confidence,
|
|
2319
|
+
explanation: `Scroll to ${target}`
|
|
2320
|
+
},
|
|
2321
|
+
shouldEscalate: confidence < threshold,
|
|
2322
|
+
rawConfidence: confidence,
|
|
2323
|
+
matchedPatterns: ['scroll', target]
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
break;
|
|
2327
|
+
}
|
|
2328
|
+
case 'highlight': {
|
|
2329
|
+
// Highlighting is typically lower confidence
|
|
2330
|
+
const target = this.extractTarget(match);
|
|
2331
|
+
const selector = this.findElementByDescription(target);
|
|
2332
|
+
if (selector) {
|
|
2333
|
+
const confidence = baseWeight * 0.85;
|
|
2334
|
+
return {
|
|
2335
|
+
action: {
|
|
2336
|
+
id: `action-${Date.now()}`,
|
|
2337
|
+
type: 'highlight',
|
|
2338
|
+
target: selector,
|
|
2339
|
+
confidence,
|
|
2340
|
+
explanation: `Highlight ${target}`
|
|
2341
|
+
},
|
|
2342
|
+
shouldEscalate: confidence < threshold,
|
|
2343
|
+
rawConfidence: confidence,
|
|
2344
|
+
matchedPatterns: ['highlight', target]
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
break;
|
|
2348
|
+
}
|
|
2349
|
+
case 'custom': {
|
|
2350
|
+
const actionName = match[1]?.toLowerCase();
|
|
2351
|
+
if (this.config.customActions?.[actionName]) {
|
|
2352
|
+
const confidence = baseWeight * 0.95;
|
|
2353
|
+
return {
|
|
2354
|
+
action: {
|
|
2355
|
+
id: `action-${Date.now()}`,
|
|
2356
|
+
type: 'custom',
|
|
2357
|
+
target: actionName,
|
|
2358
|
+
confidence,
|
|
2359
|
+
explanation: `Execute ${actionName} action`
|
|
2360
|
+
},
|
|
2361
|
+
shouldEscalate: confidence < threshold,
|
|
2362
|
+
rawConfidence: confidence,
|
|
2363
|
+
matchedPatterns: ['custom', actionName]
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
break;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* Extract target from regex match
|
|
2373
|
+
*/
|
|
2374
|
+
extractTarget(match) {
|
|
2375
|
+
// Get the last non-empty capture group
|
|
2376
|
+
for (let i = match.length - 1; i >= 1; i--) {
|
|
2377
|
+
if (match[i]) {
|
|
2378
|
+
return match[i].trim();
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
return '';
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Find site map match for target string
|
|
2385
|
+
*/
|
|
2386
|
+
findSiteMapMatch(target) {
|
|
2387
|
+
const normalizedTarget = target.toLowerCase().replace(/[^a-z0-9\s]/g, '');
|
|
2388
|
+
// Exact match
|
|
2389
|
+
const exact = this.siteMapIndex.get(normalizedTarget);
|
|
2390
|
+
if (exact) {
|
|
2391
|
+
return { entry: exact, similarity: 1.0 };
|
|
2392
|
+
}
|
|
2393
|
+
// Fuzzy match
|
|
2394
|
+
let bestMatch = null;
|
|
2395
|
+
let bestScore = 0;
|
|
2396
|
+
for (const [key, entry] of this.siteMapIndex) {
|
|
2397
|
+
const score = this.calculateSimilarity(normalizedTarget, key);
|
|
2398
|
+
if (score > bestScore && score > 0.6) {
|
|
2399
|
+
bestScore = score;
|
|
2400
|
+
bestMatch = entry;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
if (bestMatch) {
|
|
2404
|
+
return { entry: bestMatch, similarity: bestScore };
|
|
2405
|
+
}
|
|
2406
|
+
return null;
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Fuzzy match against site map for unstructured queries
|
|
2410
|
+
*/
|
|
2411
|
+
fuzzyMatchSiteMap(message) {
|
|
2412
|
+
const threshold = this.config.confidenceThreshold ?? 0.8;
|
|
2413
|
+
// Look for page/site name mentions
|
|
2414
|
+
for (const [key, entry] of this.siteMapIndex) {
|
|
2415
|
+
if (message.includes(key)) {
|
|
2416
|
+
const confidence = 0.75; // Lower confidence for implicit matches
|
|
2417
|
+
return {
|
|
2418
|
+
action: {
|
|
2419
|
+
id: `action-${Date.now()}`,
|
|
2420
|
+
type: 'navigate',
|
|
2421
|
+
target: entry.path,
|
|
2422
|
+
confidence,
|
|
2423
|
+
explanation: `Navigate to ${entry.name}`
|
|
2424
|
+
},
|
|
2425
|
+
shouldEscalate: confidence < threshold,
|
|
2426
|
+
rawConfidence: confidence,
|
|
2427
|
+
matchedPatterns: ['fuzzy-sitemap', key]
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Match custom action keywords
|
|
2435
|
+
*/
|
|
2436
|
+
matchCustomAction(message) {
|
|
2437
|
+
if (!this.config.customActions)
|
|
2438
|
+
return null;
|
|
2439
|
+
const threshold = this.config.confidenceThreshold ?? 0.8;
|
|
2440
|
+
for (const actionName of Object.keys(this.config.customActions)) {
|
|
2441
|
+
if (message.includes(actionName.toLowerCase())) {
|
|
2442
|
+
const confidence = 0.85;
|
|
2443
|
+
return {
|
|
2444
|
+
action: {
|
|
2445
|
+
id: `action-${Date.now()}`,
|
|
2446
|
+
type: 'custom',
|
|
2447
|
+
target: actionName,
|
|
2448
|
+
confidence,
|
|
2449
|
+
explanation: `Execute ${actionName}`
|
|
2450
|
+
},
|
|
2451
|
+
shouldEscalate: confidence < threshold,
|
|
2452
|
+
rawConfidence: confidence,
|
|
2453
|
+
matchedPatterns: ['custom', actionName]
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Parse form fill intent
|
|
2461
|
+
*/
|
|
2462
|
+
parseFormIntent(match) {
|
|
2463
|
+
const fullText = match[0];
|
|
2464
|
+
// Try to extract field name and value
|
|
2465
|
+
for (const [, field] of this.formIndex) {
|
|
2466
|
+
if (fullText.toLowerCase().includes(field.name.toLowerCase())) {
|
|
2467
|
+
// Extract value - everything after the field name
|
|
2468
|
+
const valueMatch = fullText.match(new RegExp(`${field.name}[^a-z]*(.+)`, 'i'));
|
|
2469
|
+
return {
|
|
2470
|
+
field,
|
|
2471
|
+
value: valueMatch?.[1]?.trim() || ''
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return { field: null, value: this.extractTarget(match) };
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Find button selector by name
|
|
2479
|
+
*/
|
|
2480
|
+
findButtonSelector(name) {
|
|
2481
|
+
const normalizedName = name.toLowerCase();
|
|
2482
|
+
// Check if we're in a browser environment
|
|
2483
|
+
if (typeof document === 'undefined') {
|
|
2484
|
+
// Return a generic selector for server-side contexts
|
|
2485
|
+
return `button:contains("${name}")`;
|
|
2486
|
+
}
|
|
2487
|
+
// Common button selectors
|
|
2488
|
+
const selectors = [
|
|
2489
|
+
`[data-action="${normalizedName}"]`,
|
|
2490
|
+
`[aria-label*="${normalizedName}" i]`,
|
|
2491
|
+
`.btn-${normalizedName}`,
|
|
2492
|
+
`#${normalizedName}-button`,
|
|
2493
|
+
`button.${normalizedName}`
|
|
2494
|
+
];
|
|
2495
|
+
// Check if any selector matches
|
|
2496
|
+
for (const selector of selectors) {
|
|
2497
|
+
try {
|
|
2498
|
+
if (document.querySelector(selector)) {
|
|
2499
|
+
return selector;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
catch {
|
|
2503
|
+
// Invalid selector, continue
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
// Fallback - try to find any button with matching text
|
|
2507
|
+
const buttons = document.querySelectorAll('button, [role="button"], a.btn');
|
|
2508
|
+
for (const btn of buttons) {
|
|
2509
|
+
if (btn.textContent?.toLowerCase().includes(normalizedName)) {
|
|
2510
|
+
return this.generateUniqueSelector(btn);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
return null;
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Find modal config by name
|
|
2517
|
+
*/
|
|
2518
|
+
findModalConfig(name) {
|
|
2519
|
+
return this.modalIndex.get(name.toLowerCase()) || null;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Find scroll target selector
|
|
2523
|
+
*/
|
|
2524
|
+
findScrollTarget(target) {
|
|
2525
|
+
const normalizedTarget = target.toLowerCase();
|
|
2526
|
+
// Special cases
|
|
2527
|
+
if (normalizedTarget === 'top' || normalizedTarget === 'beginning') {
|
|
2528
|
+
return 'body';
|
|
2529
|
+
}
|
|
2530
|
+
if (normalizedTarget === 'bottom' || normalizedTarget === 'end') {
|
|
2531
|
+
return 'body:last-child';
|
|
2532
|
+
}
|
|
2533
|
+
// Check if we're in a browser environment
|
|
2534
|
+
if (typeof document === 'undefined') {
|
|
2535
|
+
return `#${normalizedTarget.replace(/\s+/g, '-')}`;
|
|
2536
|
+
}
|
|
2537
|
+
// Try to find section by id or class
|
|
2538
|
+
const selectors = [
|
|
2539
|
+
`#${normalizedTarget.replace(/\s+/g, '-')}`,
|
|
2540
|
+
`[data-section="${normalizedTarget}"]`,
|
|
2541
|
+
`.${normalizedTarget.replace(/\s+/g, '-')}-section`,
|
|
2542
|
+
`section[aria-label*="${normalizedTarget}" i]`
|
|
2543
|
+
];
|
|
2544
|
+
for (const selector of selectors) {
|
|
2545
|
+
try {
|
|
2546
|
+
if (document.querySelector(selector)) {
|
|
2547
|
+
return selector;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
catch {
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
return null;
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Find element by description
|
|
2558
|
+
*/
|
|
2559
|
+
findElementByDescription(description) {
|
|
2560
|
+
const normalizedDesc = description.toLowerCase();
|
|
2561
|
+
// Check if we're in a browser environment
|
|
2562
|
+
if (typeof document === 'undefined') {
|
|
2563
|
+
return `[aria-label*="${normalizedDesc}" i]`;
|
|
2564
|
+
}
|
|
2565
|
+
// Try common patterns
|
|
2566
|
+
const selectors = [
|
|
2567
|
+
`[aria-label*="${normalizedDesc}" i]`,
|
|
2568
|
+
`[data-testid*="${normalizedDesc}" i]`,
|
|
2569
|
+
`[title*="${normalizedDesc}" i]`
|
|
2570
|
+
];
|
|
2571
|
+
for (const selector of selectors) {
|
|
2572
|
+
try {
|
|
2573
|
+
if (document.querySelector(selector)) {
|
|
2574
|
+
return selector;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
catch {
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
return null;
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Generate unique selector for an element
|
|
2585
|
+
*/
|
|
2586
|
+
generateUniqueSelector(element) {
|
|
2587
|
+
// Try ID first
|
|
2588
|
+
if (element.id) {
|
|
2589
|
+
return `#${element.id}`;
|
|
2590
|
+
}
|
|
2591
|
+
// Try data attributes
|
|
2592
|
+
if (element.getAttribute('data-testid')) {
|
|
2593
|
+
return `[data-testid="${element.getAttribute('data-testid')}"]`;
|
|
2594
|
+
}
|
|
2595
|
+
// Build path selector
|
|
2596
|
+
const path = [];
|
|
2597
|
+
let current = element;
|
|
2598
|
+
while (current && current !== document.body) {
|
|
2599
|
+
let selector = current.tagName.toLowerCase();
|
|
2600
|
+
if (current.className && typeof current.className === 'string') {
|
|
2601
|
+
const classes = current.className.split(' ').filter(c => c && !c.includes(' '));
|
|
2602
|
+
if (classes.length > 0) {
|
|
2603
|
+
selector += `.${classes.slice(0, 2).join('.')}`;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
path.unshift(selector);
|
|
2607
|
+
current = current.parentElement;
|
|
2608
|
+
}
|
|
2609
|
+
return path.join(' > ');
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Calculate string similarity (Jaccard index)
|
|
2613
|
+
*/
|
|
2614
|
+
calculateSimilarity(a, b) {
|
|
2615
|
+
const setA = new Set(a.split(/\s+/));
|
|
2616
|
+
const setB = new Set(b.split(/\s+/));
|
|
2617
|
+
const intersection = new Set([...setA].filter(x => setB.has(x)));
|
|
2618
|
+
const union = new Set([...setA, ...setB]);
|
|
2619
|
+
return intersection.size / union.size;
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Get configuration
|
|
2623
|
+
*/
|
|
2624
|
+
getConfig() {
|
|
2625
|
+
return this.config;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// src/agentic/CyberneticAgent.ts
|
|
2630
|
+
// DOM interaction agent for agentic actions
|
|
2631
|
+
/**
|
|
2632
|
+
* DOM interaction agent
|
|
2633
|
+
*
|
|
2634
|
+
* Executes actions on the host page based on classified intent.
|
|
2635
|
+
* Supports navigation, form filling, clicking, scrolling, and more.
|
|
2636
|
+
*/
|
|
2637
|
+
class CyberneticAgent {
|
|
2638
|
+
constructor(config) {
|
|
2639
|
+
this.highlightOverlay = null;
|
|
2640
|
+
this.actionCount = 0;
|
|
2641
|
+
this.lastActionReset = Date.now();
|
|
2642
|
+
this.config = config;
|
|
2643
|
+
this.classifier = new CyberneticIntentClassifier(config);
|
|
2644
|
+
// Inject highlight styles (only in browser)
|
|
2645
|
+
if (typeof document !== 'undefined') {
|
|
2646
|
+
this.injectStyles();
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* Interpret user message and determine action
|
|
2651
|
+
*
|
|
2652
|
+
* @param message - User's message to interpret
|
|
2653
|
+
* @returns Intent classification with suggested action
|
|
2654
|
+
*/
|
|
2655
|
+
interpretIntent(message) {
|
|
2656
|
+
return this.classifier.classify(message);
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Execute an agent action
|
|
2660
|
+
*
|
|
2661
|
+
* @param action - The action to execute
|
|
2662
|
+
* @returns Result of action execution
|
|
2663
|
+
*/
|
|
2664
|
+
async executeAction(action) {
|
|
2665
|
+
// Reset action count every minute
|
|
2666
|
+
const now = Date.now();
|
|
2667
|
+
if (now - this.lastActionReset > 60000) {
|
|
2668
|
+
this.actionCount = 0;
|
|
2669
|
+
this.lastActionReset = now;
|
|
2670
|
+
}
|
|
2671
|
+
// Check rate limit
|
|
2672
|
+
const maxActions = this.config.maxActionsPerTurn ?? 5;
|
|
2673
|
+
if (this.actionCount >= maxActions) {
|
|
2674
|
+
return {
|
|
2675
|
+
success: false,
|
|
2676
|
+
message: `Rate limit exceeded. Maximum ${maxActions} actions per minute.`
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
// Validate action is allowed
|
|
2680
|
+
if (!this.isActionAllowed(action)) {
|
|
2681
|
+
return {
|
|
2682
|
+
success: false,
|
|
2683
|
+
message: `Action type "${action.type}" is not allowed`
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
this.actionCount++;
|
|
2687
|
+
switch (action.type) {
|
|
2688
|
+
case 'navigate':
|
|
2689
|
+
return this.navigate(action.target, action.params);
|
|
2690
|
+
case 'fillForm':
|
|
2691
|
+
return this.fillForm(action.target, action.params?.value);
|
|
2692
|
+
case 'clickElement':
|
|
2693
|
+
return this.clickElement(action.target);
|
|
2694
|
+
case 'triggerModal':
|
|
2695
|
+
return this.triggerModal(action.target);
|
|
2696
|
+
case 'scroll':
|
|
2697
|
+
return this.scrollToElement(action.target);
|
|
2698
|
+
case 'highlight':
|
|
2699
|
+
return this.highlightElement(action.target);
|
|
2700
|
+
case 'custom':
|
|
2701
|
+
return this.executeCustomAction(action.target, action.params);
|
|
2702
|
+
default:
|
|
2703
|
+
return {
|
|
2704
|
+
success: false,
|
|
2705
|
+
message: `Unknown action type: ${action.type}`
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Check if an action type is allowed by configuration
|
|
2711
|
+
*/
|
|
2712
|
+
isActionAllowed(action) {
|
|
2713
|
+
const allowedActions = this.config.allowedActions;
|
|
2714
|
+
if (!allowedActions)
|
|
2715
|
+
return true; // All allowed by default
|
|
2716
|
+
const actionTypeMap = {
|
|
2717
|
+
'navigate': 'navigate',
|
|
2718
|
+
'fillForm': 'fill',
|
|
2719
|
+
'clickElement': 'click',
|
|
2720
|
+
'scroll': 'scroll',
|
|
2721
|
+
'highlight': 'click', // Highlight is a form of interaction
|
|
2722
|
+
'triggerModal': 'click',
|
|
2723
|
+
'custom': 'click'
|
|
2724
|
+
};
|
|
2725
|
+
const mappedType = actionTypeMap[action.type];
|
|
2726
|
+
return allowedActions.includes(mappedType);
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Check if selector is allowed (not blocked)
|
|
2730
|
+
*/
|
|
2731
|
+
isSelectorAllowed(selector) {
|
|
2732
|
+
// Check blocked selectors
|
|
2733
|
+
if (this.config.blockedSelectors) {
|
|
2734
|
+
for (const blocked of this.config.blockedSelectors) {
|
|
2735
|
+
if (selector.includes(blocked)) {
|
|
2736
|
+
return false;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
// Check allowed selectors (if specified, only those are allowed)
|
|
2741
|
+
if (this.config.allowedSelectors && this.config.allowedSelectors.length > 0) {
|
|
2742
|
+
return this.config.allowedSelectors.some(allowed => selector.includes(allowed));
|
|
2743
|
+
}
|
|
2744
|
+
return true;
|
|
2745
|
+
}
|
|
2746
|
+
// ==================== ACTION IMPLEMENTATIONS ====================
|
|
2747
|
+
/**
|
|
2748
|
+
* Navigate to a URL
|
|
2749
|
+
*/
|
|
2750
|
+
async navigate(path, params) {
|
|
2751
|
+
try {
|
|
2752
|
+
// Validate URL
|
|
2753
|
+
if (!this.validateNavigationUrl(path)) {
|
|
2754
|
+
return {
|
|
2755
|
+
success: false,
|
|
2756
|
+
message: 'Invalid or blocked URL'
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
// Build URL with params
|
|
2760
|
+
let url = path;
|
|
2761
|
+
if (params) {
|
|
2762
|
+
const searchParams = new URLSearchParams();
|
|
2763
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2764
|
+
if (value !== undefined && value !== null) {
|
|
2765
|
+
searchParams.set(key, String(value));
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
const queryString = searchParams.toString();
|
|
2769
|
+
if (queryString) {
|
|
2770
|
+
url += (url.includes('?') ? '&' : '?') + queryString;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
// Check if we're in a browser environment
|
|
2774
|
+
if (typeof window === 'undefined') {
|
|
2775
|
+
return {
|
|
2776
|
+
success: true,
|
|
2777
|
+
message: `Navigation to ${path} would be executed (server-side)`
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
// Check if internal or external navigation
|
|
2781
|
+
if (this.isInternalUrl(url)) {
|
|
2782
|
+
// Try client-side navigation first (SPA support)
|
|
2783
|
+
if (this.tryClientSideNavigation(url)) {
|
|
2784
|
+
return {
|
|
2785
|
+
success: true,
|
|
2786
|
+
message: `Navigated to ${path}`
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
// Fallback to full page navigation
|
|
2790
|
+
window.location.href = url;
|
|
2791
|
+
return {
|
|
2792
|
+
success: true,
|
|
2793
|
+
message: `Navigating to ${path}...`
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
// External URL - open in new tab
|
|
2797
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
2798
|
+
return {
|
|
2799
|
+
success: true,
|
|
2800
|
+
message: `Opened ${url} in new tab`
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
catch (error) {
|
|
2804
|
+
return {
|
|
2805
|
+
success: false,
|
|
2806
|
+
message: 'Navigation failed',
|
|
2807
|
+
error: String(error)
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Fill a form field
|
|
2813
|
+
*/
|
|
2814
|
+
async fillForm(selector, value) {
|
|
2815
|
+
// Check selector is allowed
|
|
2816
|
+
if (!this.isSelectorAllowed(selector)) {
|
|
2817
|
+
return {
|
|
2818
|
+
success: false,
|
|
2819
|
+
message: `Selector "${selector}" is not allowed`
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
// Sanitize selector
|
|
2823
|
+
const sanitizedSelector = this.sanitizeSelector(selector);
|
|
2824
|
+
// Check if we're in a browser environment
|
|
2825
|
+
if (typeof document === 'undefined') {
|
|
2826
|
+
return {
|
|
2827
|
+
success: true,
|
|
2828
|
+
message: `Form fill would set "${sanitizedSelector}" to "${value}" (server-side)`
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
try {
|
|
2832
|
+
const element = document.querySelector(sanitizedSelector);
|
|
2833
|
+
if (!element) {
|
|
2834
|
+
return {
|
|
2835
|
+
success: false,
|
|
2836
|
+
message: `Element not found: ${sanitizedSelector}`
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
if (!(element instanceof HTMLInputElement ||
|
|
2840
|
+
element instanceof HTMLTextAreaElement ||
|
|
2841
|
+
element instanceof HTMLSelectElement)) {
|
|
2842
|
+
return {
|
|
2843
|
+
success: false,
|
|
2844
|
+
message: 'Element is not a form field'
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
// Focus the element
|
|
2848
|
+
element.focus();
|
|
2849
|
+
// Set value
|
|
2850
|
+
if (element instanceof HTMLSelectElement) {
|
|
2851
|
+
// For select, find matching option
|
|
2852
|
+
const option = Array.from(element.options).find(opt => opt.value.toLowerCase() === value.toLowerCase() ||
|
|
2853
|
+
opt.text.toLowerCase() === value.toLowerCase());
|
|
2854
|
+
if (option) {
|
|
2855
|
+
element.value = option.value;
|
|
2856
|
+
}
|
|
2857
|
+
else {
|
|
2858
|
+
return {
|
|
2859
|
+
success: false,
|
|
2860
|
+
message: `Option "${value}" not found`
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
else {
|
|
2865
|
+
element.value = value;
|
|
2866
|
+
}
|
|
2867
|
+
// Dispatch events to trigger frameworks
|
|
2868
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2869
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2870
|
+
// For React - try to set value natively
|
|
2871
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
|
2872
|
+
if (nativeInputValueSetter && element instanceof HTMLInputElement) {
|
|
2873
|
+
nativeInputValueSetter.call(element, value);
|
|
2874
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2875
|
+
}
|
|
2876
|
+
return {
|
|
2877
|
+
success: true,
|
|
2878
|
+
message: `Filled field with "${value}"`
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
catch (error) {
|
|
2882
|
+
return {
|
|
2883
|
+
success: false,
|
|
2884
|
+
message: 'Failed to fill form field',
|
|
2885
|
+
error: String(error)
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Click an element
|
|
2891
|
+
*/
|
|
2892
|
+
async clickElement(selector) {
|
|
2893
|
+
// Check selector is allowed
|
|
2894
|
+
if (!this.isSelectorAllowed(selector)) {
|
|
2895
|
+
return {
|
|
2896
|
+
success: false,
|
|
2897
|
+
message: `Selector "${selector}" is not allowed`
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
// Sanitize selector
|
|
2901
|
+
const sanitizedSelector = this.sanitizeSelector(selector);
|
|
2902
|
+
// Check if we're in a browser environment
|
|
2903
|
+
if (typeof document === 'undefined') {
|
|
2904
|
+
return {
|
|
2905
|
+
success: true,
|
|
2906
|
+
message: `Click on "${sanitizedSelector}" would be executed (server-side)`
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
try {
|
|
2910
|
+
const element = document.querySelector(sanitizedSelector);
|
|
2911
|
+
if (!element) {
|
|
2912
|
+
return {
|
|
2913
|
+
success: false,
|
|
2914
|
+
message: `Element not found: ${sanitizedSelector}`
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
if (!(element instanceof HTMLElement)) {
|
|
2918
|
+
return {
|
|
2919
|
+
success: false,
|
|
2920
|
+
message: 'Element is not clickable'
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
// Scroll into view
|
|
2924
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
2925
|
+
// Brief delay for scroll
|
|
2926
|
+
await this.sleep(300);
|
|
2927
|
+
// Click
|
|
2928
|
+
element.click();
|
|
2929
|
+
return {
|
|
2930
|
+
success: true,
|
|
2931
|
+
message: 'Element clicked'
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
catch (error) {
|
|
2935
|
+
return {
|
|
2936
|
+
success: false,
|
|
2937
|
+
message: 'Failed to click element',
|
|
2938
|
+
error: String(error)
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Trigger a modal by clicking its trigger element
|
|
2944
|
+
*/
|
|
2945
|
+
async triggerModal(triggerSelector) {
|
|
2946
|
+
return this.clickElement(triggerSelector);
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Scroll to an element
|
|
2950
|
+
*/
|
|
2951
|
+
async scrollToElement(selector) {
|
|
2952
|
+
// Check if we're in a browser environment
|
|
2953
|
+
if (typeof window === 'undefined') {
|
|
2954
|
+
return {
|
|
2955
|
+
success: true,
|
|
2956
|
+
message: `Scroll to "${selector}" would be executed (server-side)`
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
try {
|
|
2960
|
+
// Special case for top/bottom
|
|
2961
|
+
if (selector === 'body') {
|
|
2962
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
2963
|
+
return { success: true, message: 'Scrolled to top' };
|
|
2964
|
+
}
|
|
2965
|
+
if (selector === 'body:last-child') {
|
|
2966
|
+
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
2967
|
+
return { success: true, message: 'Scrolled to bottom' };
|
|
2968
|
+
}
|
|
2969
|
+
const element = document.querySelector(selector);
|
|
2970
|
+
if (!element) {
|
|
2971
|
+
return {
|
|
2972
|
+
success: false,
|
|
2973
|
+
message: `Element not found: ${selector}`
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
2977
|
+
return {
|
|
2978
|
+
success: true,
|
|
2979
|
+
message: 'Scrolled to element'
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
catch (error) {
|
|
2983
|
+
return {
|
|
2984
|
+
success: false,
|
|
2985
|
+
message: 'Failed to scroll',
|
|
2986
|
+
error: String(error)
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* Highlight an element with animation
|
|
2992
|
+
*/
|
|
2993
|
+
async highlightElement(selector) {
|
|
2994
|
+
// Check selector is allowed
|
|
2995
|
+
if (!this.isSelectorAllowed(selector)) {
|
|
2996
|
+
return {
|
|
2997
|
+
success: false,
|
|
2998
|
+
message: `Selector "${selector}" is not allowed`
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
// Check if we're in a browser environment
|
|
3002
|
+
if (typeof document === 'undefined') {
|
|
3003
|
+
return {
|
|
3004
|
+
success: true,
|
|
3005
|
+
message: `Highlight on "${selector}" would be shown (server-side)`
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
try {
|
|
3009
|
+
const element = document.querySelector(selector);
|
|
3010
|
+
if (!element) {
|
|
3011
|
+
return {
|
|
3012
|
+
success: false,
|
|
3013
|
+
message: `Element not found: ${selector}`
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
// Remove existing highlight
|
|
3017
|
+
this.removeHighlight();
|
|
3018
|
+
// Scroll element into view
|
|
3019
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
3020
|
+
// Brief delay for scroll
|
|
3021
|
+
await this.sleep(300);
|
|
3022
|
+
// Create highlight overlay
|
|
3023
|
+
const rect = element.getBoundingClientRect();
|
|
3024
|
+
const overlay = document.createElement('div');
|
|
3025
|
+
overlay.className = 'astermind-highlight-overlay';
|
|
3026
|
+
overlay.style.cssText = `
|
|
3027
|
+
position: fixed;
|
|
3028
|
+
top: ${rect.top - 4}px;
|
|
3029
|
+
left: ${rect.left - 4}px;
|
|
3030
|
+
width: ${rect.width + 8}px;
|
|
3031
|
+
height: ${rect.height + 8}px;
|
|
3032
|
+
border: 3px solid #4F46E5;
|
|
3033
|
+
border-radius: 4px;
|
|
3034
|
+
pointer-events: none;
|
|
3035
|
+
z-index: 999999;
|
|
3036
|
+
animation: astermind-highlight-pulse 1s ease-in-out 3;
|
|
3037
|
+
`;
|
|
3038
|
+
document.body.appendChild(overlay);
|
|
3039
|
+
this.highlightOverlay = overlay;
|
|
3040
|
+
// Remove after animation
|
|
3041
|
+
setTimeout(() => this.removeHighlight(), 3000);
|
|
3042
|
+
return {
|
|
3043
|
+
success: true,
|
|
3044
|
+
message: 'Element highlighted'
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
catch (error) {
|
|
3048
|
+
return {
|
|
3049
|
+
success: false,
|
|
3050
|
+
message: 'Failed to highlight element',
|
|
3051
|
+
error: String(error)
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Execute a custom action callback
|
|
3057
|
+
*/
|
|
3058
|
+
async executeCustomAction(actionName, params) {
|
|
3059
|
+
// Validate custom action is in whitelist
|
|
3060
|
+
if (!this.isAllowedCustomAction(actionName)) {
|
|
3061
|
+
return {
|
|
3062
|
+
success: false,
|
|
3063
|
+
message: `Custom action "${actionName}" not found`
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
const callback = this.config.customActions[actionName];
|
|
3067
|
+
try {
|
|
3068
|
+
return await callback(params);
|
|
3069
|
+
}
|
|
3070
|
+
catch (error) {
|
|
3071
|
+
return {
|
|
3072
|
+
success: false,
|
|
3073
|
+
message: `Custom action "${actionName}" failed`,
|
|
3074
|
+
error: String(error)
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
// ==================== HELPER METHODS ====================
|
|
3079
|
+
/**
|
|
3080
|
+
* Check if URL is internal to the current site
|
|
3081
|
+
*/
|
|
3082
|
+
isInternalUrl(url) {
|
|
3083
|
+
// Relative URLs are internal
|
|
3084
|
+
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
|
|
3085
|
+
return true;
|
|
3086
|
+
}
|
|
3087
|
+
// Check if we're in a browser environment
|
|
3088
|
+
if (typeof window === 'undefined') {
|
|
3089
|
+
return true;
|
|
3090
|
+
}
|
|
3091
|
+
// Check if same origin
|
|
3092
|
+
try {
|
|
3093
|
+
const urlObj = new URL(url, window.location.href);
|
|
3094
|
+
return urlObj.origin === window.location.origin;
|
|
3095
|
+
}
|
|
3096
|
+
catch {
|
|
3097
|
+
return true; // Assume internal on error
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Try client-side navigation for SPAs
|
|
3102
|
+
*/
|
|
3103
|
+
tryClientSideNavigation(url) {
|
|
3104
|
+
if (typeof window === 'undefined') {
|
|
3105
|
+
return false;
|
|
3106
|
+
}
|
|
3107
|
+
// Try History API pushState
|
|
3108
|
+
try {
|
|
3109
|
+
window.history.pushState({}, '', url);
|
|
3110
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
3111
|
+
// Also dispatch hashchange for hash routing
|
|
3112
|
+
if (url.includes('#')) {
|
|
3113
|
+
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
|
3114
|
+
}
|
|
3115
|
+
return true;
|
|
3116
|
+
}
|
|
3117
|
+
catch {
|
|
3118
|
+
return false;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Validate navigation URL
|
|
3123
|
+
*/
|
|
3124
|
+
validateNavigationUrl(url) {
|
|
3125
|
+
// Reject javascript: and data: URLs
|
|
3126
|
+
if (/^(javascript|data):/i.test(url)) {
|
|
3127
|
+
return false;
|
|
3128
|
+
}
|
|
3129
|
+
// Check if we're in a browser environment
|
|
3130
|
+
if (typeof window === 'undefined') {
|
|
3131
|
+
return true;
|
|
3132
|
+
}
|
|
3133
|
+
// Only allow relative URLs or same-origin
|
|
3134
|
+
try {
|
|
3135
|
+
const urlObj = new URL(url, window.location.href);
|
|
3136
|
+
return urlObj.origin === window.location.origin || url.startsWith('/');
|
|
3137
|
+
}
|
|
3138
|
+
catch {
|
|
3139
|
+
return url.startsWith('/');
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Sanitize CSS selector to prevent injection
|
|
3144
|
+
*/
|
|
3145
|
+
sanitizeSelector(selector) {
|
|
3146
|
+
// Remove potentially dangerous characters
|
|
3147
|
+
return selector
|
|
3148
|
+
.replace(/[<>'"]/g, '')
|
|
3149
|
+
.replace(/javascript:/gi, '')
|
|
3150
|
+
.replace(/data:/gi, '')
|
|
3151
|
+
.trim();
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* Validate custom action is in whitelist
|
|
3155
|
+
*/
|
|
3156
|
+
isAllowedCustomAction(actionName) {
|
|
3157
|
+
return this.config.customActions !== undefined &&
|
|
3158
|
+
Object.prototype.hasOwnProperty.call(this.config.customActions, actionName);
|
|
3159
|
+
}
|
|
3160
|
+
/**
|
|
3161
|
+
* Remove highlight overlay
|
|
3162
|
+
*/
|
|
3163
|
+
removeHighlight() {
|
|
3164
|
+
if (this.highlightOverlay) {
|
|
3165
|
+
this.highlightOverlay.remove();
|
|
3166
|
+
this.highlightOverlay = null;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Inject required styles
|
|
3171
|
+
*/
|
|
3172
|
+
injectStyles() {
|
|
3173
|
+
if (typeof document === 'undefined') {
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
if (document.getElementById('astermind-agent-styles')) {
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
const style = document.createElement('style');
|
|
3180
|
+
style.id = 'astermind-agent-styles';
|
|
3181
|
+
style.textContent = `
|
|
3182
|
+
@keyframes astermind-highlight-pulse {
|
|
3183
|
+
0%, 100% {
|
|
3184
|
+
box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.4);
|
|
3185
|
+
}
|
|
3186
|
+
50% {
|
|
3187
|
+
box-shadow: 0 0 0 10px rgba(79, 70, 229, 0);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
.astermind-highlight-overlay {
|
|
3192
|
+
transition: all 0.2s ease-out;
|
|
3193
|
+
}
|
|
3194
|
+
`;
|
|
3195
|
+
document.head.appendChild(style);
|
|
3196
|
+
}
|
|
3197
|
+
/**
|
|
3198
|
+
* Sleep utility
|
|
3199
|
+
*/
|
|
3200
|
+
sleep(ms) {
|
|
3201
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Get agent configuration
|
|
3205
|
+
*/
|
|
3206
|
+
getConfig() {
|
|
3207
|
+
return this.config;
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Get the intent classifier
|
|
3211
|
+
*/
|
|
3212
|
+
getClassifier() {
|
|
3213
|
+
return this.classifier;
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Reset action count (for testing)
|
|
3217
|
+
*/
|
|
3218
|
+
resetActionCount() {
|
|
3219
|
+
this.actionCount = 0;
|
|
3220
|
+
this.lastActionReset = Date.now();
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
// src/agentic/register.ts
|
|
3225
|
+
// Helper for registering agentic capabilities with CyberneticClient
|
|
3226
|
+
/**
|
|
3227
|
+
* Register agentic capabilities with a CyberneticClient
|
|
3228
|
+
*
|
|
3229
|
+
* This function is the bridge between core and agentic modules.
|
|
3230
|
+
* It must be called after creating the client to enable agentic features.
|
|
3231
|
+
*
|
|
3232
|
+
* @example
|
|
3233
|
+
* ```typescript
|
|
3234
|
+
* import { CyberneticClient, registerAgenticCapabilities } from '@astermind/cybernetic-chatbot-client';
|
|
3235
|
+
*
|
|
3236
|
+
* const client = new CyberneticClient({
|
|
3237
|
+
* apiUrl: 'https://api.example.com',
|
|
3238
|
+
* apiKey: 'am_xxx_123',
|
|
3239
|
+
* agentic: {
|
|
3240
|
+
* enabled: true,
|
|
3241
|
+
* allowedActions: ['click', 'fill', 'scroll'],
|
|
3242
|
+
* confirmActions: true
|
|
3243
|
+
* }
|
|
3244
|
+
* });
|
|
3245
|
+
*
|
|
3246
|
+
* // Register agentic capabilities
|
|
3247
|
+
* registerAgenticCapabilities(client);
|
|
3248
|
+
*
|
|
3249
|
+
* // Now client supports agentic responses
|
|
3250
|
+
* const response = await client.ask('Click the Add to Cart button');
|
|
3251
|
+
* ```
|
|
3252
|
+
*
|
|
3253
|
+
* @param client - The CyberneticClient instance to register capabilities with
|
|
3254
|
+
*/
|
|
3255
|
+
function registerAgenticCapabilities(client) {
|
|
3256
|
+
const capabilities = {
|
|
3257
|
+
agent: CyberneticAgent,
|
|
3258
|
+
intentClassifier: CyberneticIntentClassifier
|
|
3259
|
+
};
|
|
3260
|
+
client.registerAgentic(capabilities);
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
// src/index.ts
|
|
3264
|
+
// Main package exports (core + tree-shakeable agentic)
|
|
3265
|
+
// Core exports
|
|
3266
|
+
function createClient(config) {
|
|
3267
|
+
return new CyberneticClient(config);
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
exports.ApiClient = ApiClient;
|
|
3271
|
+
exports.CyberneticAgent = CyberneticAgent;
|
|
3272
|
+
exports.CyberneticCache = CyberneticCache;
|
|
3273
|
+
exports.CyberneticClient = CyberneticClient;
|
|
3274
|
+
exports.CyberneticIntentClassifier = CyberneticIntentClassifier;
|
|
3275
|
+
exports.CyberneticLocalRAG = CyberneticLocalRAG;
|
|
3276
|
+
exports.LicenseManager = LicenseManager;
|
|
3277
|
+
exports.REQUIRED_FEATURES = REQUIRED_FEATURES;
|
|
3278
|
+
exports.createClient = createClient;
|
|
3279
|
+
exports.createLicenseManager = createLicenseManager;
|
|
3280
|
+
exports.detectEnvironment = detectEnvironment;
|
|
3281
|
+
exports.getEnforcementMode = getEnforcementMode;
|
|
3282
|
+
exports.getTokenExpiration = getTokenExpiration;
|
|
3283
|
+
exports.isValidJWTFormat = isValidJWTFormat;
|
|
3284
|
+
exports.loadConfig = loadConfig;
|
|
3285
|
+
exports.registerAgenticCapabilities = registerAgenticCapabilities;
|
|
3286
|
+
exports.validateConfig = validateConfig;
|
|
3287
|
+
exports.verifyLicenseToken = verifyLicenseToken;
|
|
3288
|
+
|
|
3289
|
+
}));
|
|
3290
|
+
//# sourceMappingURL=cybernetic-chatbot-client.umd.js.map
|