@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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +667 -0
  3. package/dist/ApiClient.d.ts +86 -0
  4. package/dist/ApiClient.d.ts.map +1 -0
  5. package/dist/CyberneticCache.d.ts +56 -0
  6. package/dist/CyberneticCache.d.ts.map +1 -0
  7. package/dist/CyberneticClient.d.ts +207 -0
  8. package/dist/CyberneticClient.d.ts.map +1 -0
  9. package/dist/CyberneticLocalRAG.d.ts +59 -0
  10. package/dist/CyberneticLocalRAG.d.ts.map +1 -0
  11. package/dist/agentic/CyberneticAgent.d.ts +111 -0
  12. package/dist/agentic/CyberneticAgent.d.ts.map +1 -0
  13. package/dist/agentic/CyberneticIntentClassifier.d.ts +78 -0
  14. package/dist/agentic/CyberneticIntentClassifier.d.ts.map +1 -0
  15. package/dist/agentic/index.d.ts +13 -0
  16. package/dist/agentic/index.d.ts.map +1 -0
  17. package/dist/agentic/register.d.ts +32 -0
  18. package/dist/agentic/register.d.ts.map +1 -0
  19. package/dist/agentic/tools/ClickTool.d.ts +41 -0
  20. package/dist/agentic/tools/ClickTool.d.ts.map +1 -0
  21. package/dist/agentic/tools/FillTool.d.ts +59 -0
  22. package/dist/agentic/tools/FillTool.d.ts.map +1 -0
  23. package/dist/agentic/tools/NavigateTool.d.ts +87 -0
  24. package/dist/agentic/tools/NavigateTool.d.ts.map +1 -0
  25. package/dist/agentic/tools/ScrollTool.d.ts +74 -0
  26. package/dist/agentic/tools/ScrollTool.d.ts.map +1 -0
  27. package/dist/agentic/tools/index.d.ts +9 -0
  28. package/dist/agentic/tools/index.d.ts.map +1 -0
  29. package/dist/agentic/types.d.ts +112 -0
  30. package/dist/agentic/types.d.ts.map +1 -0
  31. package/dist/config.d.ts +10 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/cybernetic-chatbot-client-full.esm.js +3271 -0
  34. package/dist/cybernetic-chatbot-client-full.esm.js.map +1 -0
  35. package/dist/cybernetic-chatbot-client-full.min.js +2 -0
  36. package/dist/cybernetic-chatbot-client-full.min.js.map +1 -0
  37. package/dist/cybernetic-chatbot-client-full.umd.js +3296 -0
  38. package/dist/cybernetic-chatbot-client-full.umd.js.map +1 -0
  39. package/dist/cybernetic-chatbot-client.esm.js +3265 -0
  40. package/dist/cybernetic-chatbot-client.esm.js.map +1 -0
  41. package/dist/cybernetic-chatbot-client.min.js +2 -0
  42. package/dist/cybernetic-chatbot-client.min.js.map +1 -0
  43. package/dist/cybernetic-chatbot-client.umd.js +3290 -0
  44. package/dist/cybernetic-chatbot-client.umd.js.map +1 -0
  45. package/dist/full.d.ts +15 -0
  46. package/dist/full.d.ts.map +1 -0
  47. package/dist/index.d.ts +15 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/license/base64url.d.ts +24 -0
  50. package/dist/license/base64url.d.ts.map +1 -0
  51. package/dist/license/index.d.ts +5 -0
  52. package/dist/license/index.d.ts.map +1 -0
  53. package/dist/license/licenseManager.d.ts +124 -0
  54. package/dist/license/licenseManager.d.ts.map +1 -0
  55. package/dist/license/types.d.ts +72 -0
  56. package/dist/license/types.d.ts.map +1 -0
  57. package/dist/license/verifier.d.ts +19 -0
  58. package/dist/license/verifier.d.ts.map +1 -0
  59. package/dist/types.d.ts +163 -0
  60. package/dist/types.d.ts.map +1 -0
  61. 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