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