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