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