@astermind/cybernetic-chatbot-client 2.2.21 → 2.2.34

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.
@@ -641,6 +641,10 @@ class CyberneticLocalRAG {
641
641
  // Tokenize and build document frequency
642
642
  const docFreq = new Map();
643
643
  for (const doc of documents) {
644
+ // Skip documents without content
645
+ if (!doc.content) {
646
+ continue;
647
+ }
644
648
  const tokens = this.tokenize(doc.content);
645
649
  const uniqueTokens = new Set(tokens);
646
650
  // Count document frequency
@@ -791,6 +795,9 @@ class CyberneticLocalRAG {
791
795
  * Tokenize text into words
792
796
  */
793
797
  tokenize(text) {
798
+ if (!text || typeof text !== 'string') {
799
+ return [];
800
+ }
794
801
  return text
795
802
  .toLowerCase()
796
803
  .replace(/[^\w\s]/g, ' ')
@@ -2663,6 +2670,68 @@ const configLoaders = {
2663
2670
 
2664
2671
  // src/agentic/SiteMapDiscovery.ts
2665
2672
  // Multi-source sitemap discovery and merging for agentic navigation
2673
+ // Zero-config mode: automatically discovers routes on any site
2674
+ // ==================== SMART DEFAULTS ====================
2675
+ /**
2676
+ * Comprehensive default selectors that work on most sites
2677
+ * Covers semantic HTML, common CSS patterns, and framework conventions
2678
+ */
2679
+ const DEFAULT_NAV_SELECTORS = [
2680
+ // Semantic HTML5 navigation
2681
+ 'nav a[href]',
2682
+ '[role="navigation"] a[href]',
2683
+ 'header a[href]',
2684
+ // Common CSS class patterns
2685
+ '.nav a[href]',
2686
+ '.navbar a[href]',
2687
+ '.navigation a[href]',
2688
+ '.sidebar a[href]',
2689
+ '.side-nav a[href]',
2690
+ '.sidenav a[href]',
2691
+ '.menu a[href]',
2692
+ '.main-menu a[href]',
2693
+ '.top-menu a[href]',
2694
+ // Data attributes
2695
+ '[data-nav] a[href]',
2696
+ '[data-menu] a[href]',
2697
+ '[data-navigation] a[href]',
2698
+ // ARIA patterns
2699
+ '[aria-label*="navigation" i] a[href]',
2700
+ '[aria-label*="menu" i] a[href]',
2701
+ // Breadcrumbs
2702
+ '[aria-label*="breadcrumb" i] a[href]',
2703
+ '.breadcrumb a[href]',
2704
+ '.breadcrumbs a[href]',
2705
+ // Generic internal links (fallback - caught last)
2706
+ 'a[href^="/"]',
2707
+ ];
2708
+ /**
2709
+ * Default paths to exclude from discovery
2710
+ * Covers common auth, API, and framework-internal routes
2711
+ */
2712
+ const DEFAULT_EXCLUDE_PATTERNS = [
2713
+ // Authentication routes
2714
+ '/login', '/logout', '/signin', '/signout', '/sign-in', '/sign-out',
2715
+ '/register', '/signup', '/sign-up', '/forgot-password', '/reset-password',
2716
+ '/auth/*', '/oauth/*', '/callback/*', '/sso/*',
2717
+ // API and framework internals
2718
+ '/api/*', '/_next/*', '/_nuxt/*', '/__*',
2719
+ // Special links
2720
+ 'javascript:*', 'mailto:*', 'tel:*', '#*',
2721
+ ];
2722
+ /**
2723
+ * Default maximum number of links to scan from DOM
2724
+ * Prevents performance issues on sites with many links
2725
+ */
2726
+ const DEFAULT_MAX_DOM_LINKS = 200;
2727
+ /**
2728
+ * Default cache TTL for localStorage (5 minutes)
2729
+ */
2730
+ const DEFAULT_CACHE_TTL = 5 * 60 * 1000;
2731
+ /**
2732
+ * Cache version - increment when cache format changes
2733
+ */
2734
+ const CACHE_VERSION = '1';
2666
2735
  /**
2667
2736
  * Multi-source sitemap discovery and management
2668
2737
  *
@@ -2670,6 +2739,9 @@ const configLoaders = {
2670
2739
  * 1. Static props (explicit configuration)
2671
2740
  * 2. Framework auto-discovery (React Router, Vue Router, etc.)
2672
2741
  * 3. Backend API (tenant-specific sitemaps)
2742
+ *
2743
+ * Zero-config mode: When agentic is enabled but no siteMapConfig provided,
2744
+ * automatically discovers routes using smart defaults.
2673
2745
  */
2674
2746
  class SiteMapDiscovery {
2675
2747
  constructor(config, apiUrl, apiKey) {
@@ -2679,6 +2751,8 @@ class SiteMapDiscovery {
2679
2751
  this.backendCache = null;
2680
2752
  this.isInitialized = false;
2681
2753
  this.initPromise = null;
2754
+ this.detectedFramework = 'generic';
2755
+ this.popstateHandler = null;
2682
2756
  this.config = config;
2683
2757
  this.apiUrl = apiUrl;
2684
2758
  this.apiKey = apiKey;
@@ -2687,6 +2761,7 @@ class SiteMapDiscovery {
2687
2761
  this.staticEntries = this.enhanceEntries(config.static, 'props');
2688
2762
  }
2689
2763
  }
2764
+ // ==================== INITIALIZATION ====================
2690
2765
  /**
2691
2766
  * Initialize all sitemap sources
2692
2767
  * Call this during widget initialization or first query based on loadStrategy
@@ -2701,8 +2776,19 @@ class SiteMapDiscovery {
2701
2776
  this.isInitialized = true;
2702
2777
  }
2703
2778
  async performInitialization() {
2779
+ // Wait for DOM to be ready before discovery
2780
+ await this.waitForDOMReady();
2781
+ // Try to load from localStorage cache first
2782
+ const cachedEntries = this.loadFromLocalStorage();
2783
+ if (cachedEntries) {
2784
+ this.discoveredEntries = cachedEntries;
2785
+ console.log(`[SiteMapDiscovery] Loaded ${cachedEntries.length} entries from cache`);
2786
+ // Still run discovery in background to refresh cache
2787
+ this.refreshInBackground();
2788
+ return;
2789
+ }
2704
2790
  const tasks = [];
2705
- // Source 2: Framework auto-discovery
2791
+ // Source 2: Framework auto-discovery (enabled by default)
2706
2792
  if (this.config.discovery?.enabled !== false) {
2707
2793
  tasks.push(this.discoverFromFramework());
2708
2794
  }
@@ -2716,6 +2802,8 @@ class SiteMapDiscovery {
2716
2802
  const { entries, source } = result.value;
2717
2803
  if (source === 'discovery') {
2718
2804
  this.discoveredEntries = entries;
2805
+ // Save to localStorage
2806
+ this.saveToLocalStorage(entries);
2719
2807
  }
2720
2808
  else if (source === 'backend') {
2721
2809
  this.backendEntries = entries;
@@ -2725,9 +2813,144 @@ class SiteMapDiscovery {
2725
2813
  console.warn('[SiteMapDiscovery] Source failed:', result.reason);
2726
2814
  }
2727
2815
  }
2816
+ // Set up popstate listener for SPA navigation
2817
+ this.setupPopstateListener();
2728
2818
  const total = this.staticEntries.length + this.discoveredEntries.length + this.backendEntries.length;
2729
2819
  console.log(`[SiteMapDiscovery] Initialized with ${total} total entries (${this.staticEntries.length} static, ${this.discoveredEntries.length} discovered, ${this.backendEntries.length} backend)`);
2730
2820
  }
2821
+ /**
2822
+ * Wait for DOM to be ready before scanning
2823
+ */
2824
+ async waitForDOMReady() {
2825
+ if (typeof document === 'undefined')
2826
+ return;
2827
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
2828
+ // DOM is ready, but give frameworks a microtask to mount
2829
+ await new Promise(resolve => setTimeout(resolve, 0));
2830
+ return;
2831
+ }
2832
+ return new Promise((resolve) => {
2833
+ document.addEventListener('DOMContentLoaded', () => resolve(), { once: true });
2834
+ });
2835
+ }
2836
+ /**
2837
+ * Set up popstate listener to re-discover on SPA navigation
2838
+ */
2839
+ setupPopstateListener() {
2840
+ if (typeof window === 'undefined')
2841
+ return;
2842
+ this.popstateHandler = () => {
2843
+ // Debounce re-discovery
2844
+ setTimeout(() => {
2845
+ this.refreshInBackground();
2846
+ }, 100);
2847
+ };
2848
+ window.addEventListener('popstate', this.popstateHandler);
2849
+ }
2850
+ /**
2851
+ * Refresh discovery in background without blocking
2852
+ */
2853
+ async refreshInBackground() {
2854
+ try {
2855
+ const result = await this.discoverFromFramework();
2856
+ if (result.entries.length > 0) {
2857
+ this.discoveredEntries = result.entries;
2858
+ this.saveToLocalStorage(result.entries);
2859
+ }
2860
+ }
2861
+ catch (error) {
2862
+ // Silent fail for background refresh
2863
+ console.debug('[SiteMapDiscovery] Background refresh failed:', error);
2864
+ }
2865
+ }
2866
+ // ==================== LOCALSTORAGE CACHING ====================
2867
+ /**
2868
+ * Get localStorage cache key for current origin
2869
+ */
2870
+ getCacheKey() {
2871
+ if (typeof window === 'undefined')
2872
+ return 'astermind-sitemap';
2873
+ return `astermind-sitemap-${window.location.origin}`;
2874
+ }
2875
+ /**
2876
+ * Load discovered entries from localStorage cache
2877
+ */
2878
+ loadFromLocalStorage() {
2879
+ if (typeof localStorage === 'undefined')
2880
+ return null;
2881
+ try {
2882
+ const key = this.getCacheKey();
2883
+ const cached = localStorage.getItem(key);
2884
+ if (!cached)
2885
+ return null;
2886
+ const data = JSON.parse(cached);
2887
+ // Check version
2888
+ if (data.version !== CACHE_VERSION) {
2889
+ localStorage.removeItem(key);
2890
+ return null;
2891
+ }
2892
+ // Check TTL
2893
+ const ttl = this.config.discovery?.cacheTtl ?? DEFAULT_CACHE_TTL;
2894
+ if (Date.now() - data.timestamp > ttl) {
2895
+ localStorage.removeItem(key);
2896
+ return null;
2897
+ }
2898
+ this.detectedFramework = data.framework;
2899
+ return data.entries;
2900
+ }
2901
+ catch (error) {
2902
+ console.debug('[SiteMapDiscovery] Failed to load from cache:', error);
2903
+ return null;
2904
+ }
2905
+ }
2906
+ /**
2907
+ * Save discovered entries to localStorage cache
2908
+ */
2909
+ saveToLocalStorage(entries) {
2910
+ if (typeof localStorage === 'undefined')
2911
+ return;
2912
+ if (this.config.discovery?.cacheRoutes === false)
2913
+ return;
2914
+ try {
2915
+ const key = this.getCacheKey();
2916
+ const data = {
2917
+ entries,
2918
+ url: typeof window !== 'undefined' ? window.location.href : '',
2919
+ timestamp: Date.now(),
2920
+ framework: this.detectedFramework,
2921
+ version: CACHE_VERSION
2922
+ };
2923
+ localStorage.setItem(key, JSON.stringify(data));
2924
+ }
2925
+ catch (error) {
2926
+ // localStorage might be full or disabled
2927
+ console.debug('[SiteMapDiscovery] Failed to save to cache:', error);
2928
+ }
2929
+ }
2930
+ /**
2931
+ * Clear localStorage cache
2932
+ */
2933
+ clearCache() {
2934
+ if (typeof localStorage === 'undefined')
2935
+ return;
2936
+ localStorage.removeItem(this.getCacheKey());
2937
+ }
2938
+ // ==================== CLEANUP ====================
2939
+ /**
2940
+ * Dispose of resources (event listeners, etc.)
2941
+ */
2942
+ dispose() {
2943
+ if (this.popstateHandler && typeof window !== 'undefined') {
2944
+ window.removeEventListener('popstate', this.popstateHandler);
2945
+ this.popstateHandler = null;
2946
+ }
2947
+ }
2948
+ /**
2949
+ * Get detected framework name
2950
+ */
2951
+ getDetectedFramework() {
2952
+ return this.detectedFramework;
2953
+ }
2731
2954
  /**
2732
2955
  * Get merged sitemap entries with deduplication
2733
2956
  */
@@ -2754,6 +2977,7 @@ class SiteMapDiscovery {
2754
2977
  try {
2755
2978
  // Detect framework if not specified
2756
2979
  const framework = config.framework || this.detectFramework();
2980
+ this.detectedFramework = framework;
2757
2981
  let routes = [];
2758
2982
  switch (framework) {
2759
2983
  case 'react-router':
@@ -2937,26 +3161,31 @@ class SiteMapDiscovery {
2937
3161
  }
2938
3162
  /**
2939
3163
  * Discover routes by scanning DOM navigation elements
3164
+ * Uses smart defaults when no selectors provided
2940
3165
  */
2941
3166
  discoverFromDOM(selectors) {
2942
3167
  const routes = [];
2943
3168
  if (typeof document === 'undefined')
2944
3169
  return routes;
2945
- const defaultSelectors = [
2946
- 'nav a[href]',
2947
- '[role="navigation"] a[href]',
2948
- '.nav a[href]',
2949
- '.navigation a[href]',
2950
- '.sidebar a[href]',
2951
- '.menu a[href]',
2952
- 'header a[href]'
2953
- ];
2954
- const allSelectors = selectors || defaultSelectors;
3170
+ // Use provided selectors or smart defaults
3171
+ const allSelectors = selectors || DEFAULT_NAV_SELECTORS;
3172
+ // Get max links limit from config or use default
3173
+ const maxLinks = this.config.discovery?.maxDomLinks ?? DEFAULT_MAX_DOM_LINKS;
3174
+ // Get exclude patterns (merge user-provided with defaults)
3175
+ const userExcludes = this.config.discovery?.excludePaths || [];
3176
+ const allExcludes = [...DEFAULT_EXCLUDE_PATTERNS, ...userExcludes];
2955
3177
  const seen = new Set();
3178
+ let linkCount = 0;
2956
3179
  for (const selector of allSelectors) {
3180
+ // Stop if we've hit the limit
3181
+ if (linkCount >= maxLinks)
3182
+ break;
2957
3183
  try {
2958
3184
  const links = document.querySelectorAll(selector);
2959
3185
  for (const link of links) {
3186
+ // Stop if we've hit the limit
3187
+ if (linkCount >= maxLinks)
3188
+ break;
2960
3189
  const href = link.getAttribute('href');
2961
3190
  if (!href)
2962
3191
  continue;
@@ -2965,14 +3194,31 @@ class SiteMapDiscovery {
2965
3194
  continue;
2966
3195
  }
2967
3196
  // Normalize path
2968
- const path = href.startsWith('/') ? href : `/${href}`;
2969
- // Skip duplicates, anchors, and special links
2970
- if (seen.has(path) || path.startsWith('/#') || path === '/' || path.includes('javascript:')) {
2971
- continue;
3197
+ let path = href;
3198
+ if (href.startsWith('http')) {
3199
+ try {
3200
+ path = new URL(href).pathname;
3201
+ }
3202
+ catch {
3203
+ continue;
3204
+ }
3205
+ }
3206
+ else if (!href.startsWith('/')) {
3207
+ path = `/${href}`;
2972
3208
  }
2973
- seen.add(path);
2974
3209
  // Clean path (remove query string and hash)
2975
3210
  const cleanPath = path.split('?')[0].split('#')[0];
3211
+ // Skip duplicates
3212
+ if (seen.has(cleanPath))
3213
+ continue;
3214
+ // Skip root path
3215
+ if (cleanPath === '/')
3216
+ continue;
3217
+ // Skip paths matching exclude patterns
3218
+ if (this.shouldExcludeRoute(cleanPath, allExcludes))
3219
+ continue;
3220
+ seen.add(cleanPath);
3221
+ linkCount++;
2976
3222
  routes.push({
2977
3223
  path: cleanPath,
2978
3224
  name: link.textContent?.trim() || this.pathToName(cleanPath)
@@ -2983,6 +3229,7 @@ class SiteMapDiscovery {
2983
3229
  // Invalid selector - skip
2984
3230
  }
2985
3231
  }
3232
+ console.log(`[SiteMapDiscovery] DOM scan found ${routes.length} routes (limit: ${maxLinks})`);
2986
3233
  return routes;
2987
3234
  }
2988
3235
  // ==================== SOURCE 3: BACKEND API ====================
@@ -3270,9 +3517,18 @@ class CyberneticIntentClassifier {
3270
3517
  this.modalIndex = new Map();
3271
3518
  // Build indexes for fast lookup
3272
3519
  this.buildIndexes();
3273
- // Initialize multi-source sitemap discovery if configured
3274
- if (config.siteMapConfig) {
3275
- this.siteMapDiscovery = new SiteMapDiscovery(config.siteMapConfig, apiUrl, apiKey);
3520
+ // Auto-enable sitemap discovery when agentic is enabled but no explicit config provided
3521
+ // This enables zero-config mode: just set enabled: true and discovery works automatically
3522
+ const siteMapConfig = config.siteMapConfig ?? (config.enabled && !config.siteMap?.length
3523
+ ? { discovery: { enabled: true } }
3524
+ : undefined);
3525
+ // Initialize multi-source sitemap discovery
3526
+ if (siteMapConfig) {
3527
+ this.siteMapDiscovery = new SiteMapDiscovery(siteMapConfig, apiUrl, apiKey);
3528
+ // Log zero-config mode activation
3529
+ if (!config.siteMapConfig && config.enabled) {
3530
+ console.log('[CyberneticIntentClassifier] Zero-config mode: auto-discovery enabled');
3531
+ }
3276
3532
  }
3277
3533
  }
3278
3534
  /**