@astermind/cybernetic-chatbot-client 2.2.14 → 2.2.21

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.
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('fs'), require('path')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'fs', 'path'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.AsterMindCybernetic = {}));
5
- })(this, (function (exports) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('fs'), require('path')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'react', 'fs', 'path'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.AsterMindCybernetic = {}, global.react));
5
+ })(this, (function (exports, react) { 'use strict';
6
6
 
7
7
  // src/ApiClient.ts
8
8
  // HTTP client for AsterMind backend API
@@ -2663,6 +2663,555 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2663
2663
  loadFromScriptAttributes
2664
2664
  };
2665
2665
 
2666
+ // src/agentic/SiteMapDiscovery.ts
2667
+ // Multi-source sitemap discovery and merging for agentic navigation
2668
+ /**
2669
+ * Multi-source sitemap discovery and management
2670
+ *
2671
+ * Combines routes from three sources:
2672
+ * 1. Static props (explicit configuration)
2673
+ * 2. Framework auto-discovery (React Router, Vue Router, etc.)
2674
+ * 3. Backend API (tenant-specific sitemaps)
2675
+ */
2676
+ class SiteMapDiscovery {
2677
+ constructor(config, apiUrl, apiKey) {
2678
+ this.staticEntries = [];
2679
+ this.discoveredEntries = [];
2680
+ this.backendEntries = [];
2681
+ this.backendCache = null;
2682
+ this.isInitialized = false;
2683
+ this.initPromise = null;
2684
+ this.config = config;
2685
+ this.apiUrl = apiUrl;
2686
+ this.apiKey = apiKey;
2687
+ // Load static entries immediately (synchronous)
2688
+ if (config.static) {
2689
+ this.staticEntries = this.enhanceEntries(config.static, 'props');
2690
+ }
2691
+ }
2692
+ /**
2693
+ * Initialize all sitemap sources
2694
+ * Call this during widget initialization or first query based on loadStrategy
2695
+ */
2696
+ async initialize() {
2697
+ if (this.isInitialized)
2698
+ return;
2699
+ if (this.initPromise)
2700
+ return this.initPromise;
2701
+ this.initPromise = this.performInitialization();
2702
+ await this.initPromise;
2703
+ this.isInitialized = true;
2704
+ }
2705
+ async performInitialization() {
2706
+ const tasks = [];
2707
+ // Source 2: Framework auto-discovery
2708
+ if (this.config.discovery?.enabled !== false) {
2709
+ tasks.push(this.discoverFromFramework());
2710
+ }
2711
+ // Source 3: Backend API
2712
+ if (this.config.backend?.enabled !== false && this.config.backend?.endpoint) {
2713
+ tasks.push(this.fetchFromBackend());
2714
+ }
2715
+ const results = await Promise.allSettled(tasks);
2716
+ for (const result of results) {
2717
+ if (result.status === 'fulfilled') {
2718
+ const { entries, source } = result.value;
2719
+ if (source === 'discovery') {
2720
+ this.discoveredEntries = entries;
2721
+ }
2722
+ else if (source === 'backend') {
2723
+ this.backendEntries = entries;
2724
+ }
2725
+ }
2726
+ else {
2727
+ console.warn('[SiteMapDiscovery] Source failed:', result.reason);
2728
+ }
2729
+ }
2730
+ const total = this.staticEntries.length + this.discoveredEntries.length + this.backendEntries.length;
2731
+ console.log(`[SiteMapDiscovery] Initialized with ${total} total entries (${this.staticEntries.length} static, ${this.discoveredEntries.length} discovered, ${this.backendEntries.length} backend)`);
2732
+ }
2733
+ /**
2734
+ * Get merged sitemap entries with deduplication
2735
+ */
2736
+ getMergedEntries() {
2737
+ return this.mergeEntries(this.staticEntries, this.discoveredEntries, this.backendEntries);
2738
+ }
2739
+ /**
2740
+ * Convert to legacy AgenticSiteMapEntry format for classifier compatibility
2741
+ */
2742
+ getLegacyEntries() {
2743
+ return this.getMergedEntries().map(e => ({
2744
+ path: e.path,
2745
+ name: e.name,
2746
+ description: e.description,
2747
+ params: e.params,
2748
+ dynamicParams: e.dynamicParams,
2749
+ aliases: e.aliases
2750
+ }));
2751
+ }
2752
+ // ==================== SOURCE 2: FRAMEWORK DISCOVERY ====================
2753
+ async discoverFromFramework() {
2754
+ const config = this.config.discovery || {};
2755
+ const entries = [];
2756
+ try {
2757
+ // Detect framework if not specified
2758
+ const framework = config.framework || this.detectFramework();
2759
+ let routes = [];
2760
+ switch (framework) {
2761
+ case 'react-router':
2762
+ routes = this.discoverReactRouter();
2763
+ break;
2764
+ case 'vue-router':
2765
+ routes = this.discoverVueRouter();
2766
+ break;
2767
+ case 'next':
2768
+ routes = this.discoverNextRoutes();
2769
+ break;
2770
+ case 'angular':
2771
+ routes = this.discoverAngularRoutes();
2772
+ break;
2773
+ default:
2774
+ if (config.domFallback !== false) {
2775
+ routes = this.discoverFromDOM(config.navSelectors);
2776
+ }
2777
+ }
2778
+ // Transform routes to sitemap entries
2779
+ for (const route of routes) {
2780
+ // Apply exclude patterns
2781
+ if (this.shouldExcludeRoute(route.path, config.excludePaths)) {
2782
+ continue;
2783
+ }
2784
+ // Apply custom transform if provided
2785
+ let entry;
2786
+ if (config.transformRoute) {
2787
+ entry = config.transformRoute(route);
2788
+ }
2789
+ else {
2790
+ entry = this.defaultRouteTransform(route);
2791
+ }
2792
+ if (entry) {
2793
+ entries.push(this.enhanceEntry(entry, 'discovery'));
2794
+ }
2795
+ }
2796
+ console.log(`[SiteMapDiscovery] Discovered ${entries.length} routes from ${framework}`);
2797
+ }
2798
+ catch (error) {
2799
+ console.warn('[SiteMapDiscovery] Framework discovery failed:', error);
2800
+ return {
2801
+ entries: [],
2802
+ source: 'discovery',
2803
+ timestamp: Date.now(),
2804
+ error: String(error)
2805
+ };
2806
+ }
2807
+ return {
2808
+ entries,
2809
+ source: 'discovery',
2810
+ timestamp: Date.now()
2811
+ };
2812
+ }
2813
+ /**
2814
+ * Detect JavaScript framework from global objects
2815
+ */
2816
+ detectFramework() {
2817
+ if (typeof window === 'undefined')
2818
+ return 'generic';
2819
+ const win = window;
2820
+ // React Router v6.4+ data router
2821
+ if (win.__REACT_ROUTER_DATA_ROUTER__) {
2822
+ return 'react-router';
2823
+ }
2824
+ // React Router v5 context
2825
+ if (win.__reactRouterContext) {
2826
+ return 'react-router';
2827
+ }
2828
+ // Vue Router
2829
+ if (win.__VUE_ROUTER__ || win.__VUE_APP__?.$router) {
2830
+ return 'vue-router';
2831
+ }
2832
+ // Next.js
2833
+ if (win.__NEXT_DATA__) {
2834
+ return 'next';
2835
+ }
2836
+ // Angular
2837
+ if (win.ng || document.querySelector('[ng-version]')) {
2838
+ return 'angular';
2839
+ }
2840
+ return 'generic';
2841
+ }
2842
+ /**
2843
+ * Discover routes from React Router
2844
+ */
2845
+ discoverReactRouter() {
2846
+ const routes = [];
2847
+ if (typeof window === 'undefined')
2848
+ return routes;
2849
+ const win = window;
2850
+ // Try React Router v6.4+ data router
2851
+ const dataRouter = win.__REACT_ROUTER_DATA_ROUTER__;
2852
+ if (dataRouter?.routes) {
2853
+ this.extractRoutesFromConfig(dataRouter.routes, routes, '');
2854
+ return routes;
2855
+ }
2856
+ // Fallback: Try to find routes from __REACT_DEVTOOLS_GLOBAL_HOOK__
2857
+ // This is less reliable but can work in some cases
2858
+ return routes;
2859
+ }
2860
+ /**
2861
+ * Extract routes recursively from React Router config
2862
+ */
2863
+ extractRoutesFromConfig(routeConfigs, routes, parentPath) {
2864
+ for (const config of routeConfigs) {
2865
+ const path = this.normalizePath(parentPath, config.path || '');
2866
+ // Extract route info from handle metadata
2867
+ const route = {
2868
+ path,
2869
+ name: config.handle?.name || config.handle?.title || config.id,
2870
+ meta: config.handle || {},
2871
+ children: []
2872
+ };
2873
+ // Only include routes with paths (skip layout routes without path)
2874
+ if (config.path || config.index) {
2875
+ routes.push(route);
2876
+ }
2877
+ // Recursively process children
2878
+ if (config.children) {
2879
+ this.extractRoutesFromConfig(config.children, routes, path);
2880
+ }
2881
+ }
2882
+ }
2883
+ /**
2884
+ * Discover routes from Vue Router
2885
+ */
2886
+ discoverVueRouter() {
2887
+ const routes = [];
2888
+ if (typeof window === 'undefined')
2889
+ return routes;
2890
+ const win = window;
2891
+ // Access Vue Router instance
2892
+ const app = win.__VUE_APP__;
2893
+ const router = app?.$router || win.__VUE_ROUTER__;
2894
+ if (router?.getRoutes) {
2895
+ const vueRoutes = router.getRoutes();
2896
+ for (const vueRoute of vueRoutes) {
2897
+ routes.push({
2898
+ path: vueRoute.path,
2899
+ name: vueRoute.name?.toString(),
2900
+ meta: vueRoute.meta || {},
2901
+ component: vueRoute.components?.default?.name
2902
+ });
2903
+ }
2904
+ }
2905
+ return routes;
2906
+ }
2907
+ /**
2908
+ * Discover routes from Next.js
2909
+ */
2910
+ discoverNextRoutes() {
2911
+ const routes = [];
2912
+ if (typeof window === 'undefined')
2913
+ return routes;
2914
+ const win = window;
2915
+ const nextData = win.__NEXT_DATA__;
2916
+ if (!nextData)
2917
+ return routes;
2918
+ // Next.js doesn't expose routes directly in client
2919
+ // We can extract from build manifest if available
2920
+ const pages = nextData.buildManifest?.pages || {};
2921
+ for (const pagePath of Object.keys(pages)) {
2922
+ if (pagePath.startsWith('/_'))
2923
+ continue; // Skip internal pages
2924
+ routes.push({
2925
+ path: pagePath,
2926
+ name: this.pathToName(pagePath)
2927
+ });
2928
+ }
2929
+ return routes;
2930
+ }
2931
+ /**
2932
+ * Discover routes from Angular Router
2933
+ */
2934
+ discoverAngularRoutes() {
2935
+ // Angular route discovery requires access to Router service
2936
+ // which is typically not exposed globally
2937
+ // Users should provide routes via props or backend
2938
+ return [];
2939
+ }
2940
+ /**
2941
+ * Discover routes by scanning DOM navigation elements
2942
+ */
2943
+ discoverFromDOM(selectors) {
2944
+ const routes = [];
2945
+ if (typeof document === 'undefined')
2946
+ 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;
2957
+ const seen = new Set();
2958
+ for (const selector of allSelectors) {
2959
+ try {
2960
+ const links = document.querySelectorAll(selector);
2961
+ for (const link of links) {
2962
+ const href = link.getAttribute('href');
2963
+ if (!href)
2964
+ continue;
2965
+ // Only process internal links
2966
+ if (href.startsWith('http') && !href.startsWith(window.location.origin)) {
2967
+ continue;
2968
+ }
2969
+ // 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;
2974
+ }
2975
+ seen.add(path);
2976
+ // Clean path (remove query string and hash)
2977
+ const cleanPath = path.split('?')[0].split('#')[0];
2978
+ routes.push({
2979
+ path: cleanPath,
2980
+ name: link.textContent?.trim() || this.pathToName(cleanPath)
2981
+ });
2982
+ }
2983
+ }
2984
+ catch (e) {
2985
+ // Invalid selector - skip
2986
+ }
2987
+ }
2988
+ return routes;
2989
+ }
2990
+ // ==================== SOURCE 3: BACKEND API ====================
2991
+ async fetchFromBackend() {
2992
+ const config = this.config.backend || {};
2993
+ const endpoint = config.endpoint || '/api/external/sitemap';
2994
+ // Check cache
2995
+ if (this.backendCache && Date.now() < this.backendCache.expiresAt) {
2996
+ return {
2997
+ entries: this.backendCache.entries,
2998
+ source: 'backend',
2999
+ timestamp: this.backendCache.fetchedAt
3000
+ };
3001
+ }
3002
+ try {
3003
+ const url = new URL(endpoint, this.apiUrl);
3004
+ // Add query params
3005
+ if (config.tenantScoped !== false) {
3006
+ url.searchParams.set('tenantScoped', 'true');
3007
+ }
3008
+ if (config.includeGeneral !== false) {
3009
+ url.searchParams.set('includeGeneral', 'true');
3010
+ }
3011
+ const response = await fetch(url.toString(), {
3012
+ method: 'GET',
3013
+ headers: {
3014
+ 'X-API-Key': this.apiKey,
3015
+ 'Content-Type': 'application/json',
3016
+ ...config.headers
3017
+ }
3018
+ });
3019
+ if (!response.ok) {
3020
+ throw new Error(`Backend sitemap fetch failed: ${response.status}`);
3021
+ }
3022
+ const data = await response.json();
3023
+ const entries = this.transformBackendResponse(data);
3024
+ // Update cache
3025
+ const ttl = config.cacheTtl || 300000; // 5 minutes default
3026
+ this.backendCache = {
3027
+ entries,
3028
+ fetchedAt: Date.now(),
3029
+ expiresAt: Date.now() + ttl
3030
+ };
3031
+ console.log(`[SiteMapDiscovery] Loaded ${entries.length} routes from backend`);
3032
+ return {
3033
+ entries,
3034
+ source: 'backend',
3035
+ timestamp: Date.now()
3036
+ };
3037
+ }
3038
+ catch (error) {
3039
+ console.warn('[SiteMapDiscovery] Backend fetch failed:', error);
3040
+ return {
3041
+ entries: this.backendCache?.entries || [],
3042
+ source: 'backend',
3043
+ timestamp: Date.now(),
3044
+ error: String(error)
3045
+ };
3046
+ }
3047
+ }
3048
+ /**
3049
+ * Transform backend response to enhanced entries
3050
+ */
3051
+ transformBackendResponse(data) {
3052
+ // Handle array or object with entries/sitemap key
3053
+ const rawEntries = Array.isArray(data)
3054
+ ? data
3055
+ : data.entries || data.sitemap || data.routes || [];
3056
+ return rawEntries.map((entry) => this.enhanceEntry({
3057
+ path: entry.url || entry.path,
3058
+ name: entry.title || entry.name,
3059
+ description: entry.description,
3060
+ aliases: entry.keywords || entry.aliases,
3061
+ params: entry.params,
3062
+ dynamicParams: entry.dynamicParams
3063
+ }, 'backend'));
3064
+ }
3065
+ // ==================== MERGE LOGIC ====================
3066
+ /**
3067
+ * Merge entries from all sources with deduplication
3068
+ */
3069
+ mergeEntries(...sources) {
3070
+ const mergeConfig = this.config.merge || {};
3071
+ const priorityOrder = mergeConfig.sourcePriority || ['props', 'backend', 'discovery'];
3072
+ const dedupeStrategy = mergeConfig.deduplication || 'path';
3073
+ // Create priority map (higher number = higher priority)
3074
+ const priorityMap = new Map();
3075
+ priorityOrder.forEach((source, index) => {
3076
+ priorityMap.set(source, priorityOrder.length - index);
3077
+ });
3078
+ // Flatten all entries with priority scores
3079
+ const allEntries = [];
3080
+ for (const sourceEntries of sources) {
3081
+ for (const entry of sourceEntries) {
3082
+ const priority = priorityMap.get(entry._source || 'props') || 0;
3083
+ allEntries.push({ ...entry, _priority: priority });
3084
+ }
3085
+ }
3086
+ // Sort by priority (higher first)
3087
+ allEntries.sort((a, b) => (b._priority || 0) - (a._priority || 0));
3088
+ // Deduplicate
3089
+ const seen = new Map();
3090
+ for (const entry of allEntries) {
3091
+ let key;
3092
+ switch (dedupeStrategy) {
3093
+ case 'name':
3094
+ key = entry.name.toLowerCase();
3095
+ break;
3096
+ case 'both':
3097
+ key = `${entry.path}|${entry.name.toLowerCase()}`;
3098
+ break;
3099
+ case 'path':
3100
+ default:
3101
+ key = this.normalizePath('', entry.path);
3102
+ }
3103
+ if (!seen.has(key)) {
3104
+ seen.set(key, entry);
3105
+ }
3106
+ else if (mergeConfig.keepNonConflicting !== false) {
3107
+ // Merge non-conflicting fields from lower priority entry
3108
+ const existing = seen.get(key);
3109
+ if (!existing.description && entry.description) {
3110
+ existing.description = entry.description;
3111
+ }
3112
+ if (!existing.aliases?.length && entry.aliases?.length) {
3113
+ existing.aliases = entry.aliases;
3114
+ }
3115
+ }
3116
+ }
3117
+ return Array.from(seen.values());
3118
+ }
3119
+ // ==================== HELPERS ====================
3120
+ enhanceEntries(entries, source) {
3121
+ return entries.map(e => this.enhanceEntry(e, source));
3122
+ }
3123
+ enhanceEntry(entry, source) {
3124
+ return {
3125
+ ...entry,
3126
+ _source: source,
3127
+ _discoveredAt: Date.now()
3128
+ };
3129
+ }
3130
+ /**
3131
+ * Default transformation from DiscoveredRoute to AgenticSiteMapEntry
3132
+ */
3133
+ defaultRouteTransform(route) {
3134
+ // Skip routes without meaningful paths
3135
+ if (!route.path || route.path === '*' || route.path === '**') {
3136
+ return null;
3137
+ }
3138
+ return {
3139
+ path: route.path,
3140
+ name: route.name || route.meta?.title || this.pathToName(route.path),
3141
+ description: route.meta?.description,
3142
+ aliases: route.meta?.keywords || route.meta?.aliases,
3143
+ dynamicParams: route.path.includes(':') || route.path.includes('[')
3144
+ };
3145
+ }
3146
+ /**
3147
+ * Normalize path by combining parent and child paths
3148
+ */
3149
+ normalizePath(parent, path) {
3150
+ if (!path || path === '/')
3151
+ return parent || '/';
3152
+ if (path.startsWith('/'))
3153
+ return path;
3154
+ if (!parent || parent === '/')
3155
+ return `/${path}`;
3156
+ return `${parent}/${path}`.replace(/\/+/g, '/');
3157
+ }
3158
+ /**
3159
+ * Convert path to human-readable name
3160
+ */
3161
+ pathToName(path) {
3162
+ const segments = path.split('/').filter(Boolean);
3163
+ const last = segments[segments.length - 1] || 'Home';
3164
+ return last
3165
+ .replace(/[-_]/g, ' ')
3166
+ .replace(/\[.*?\]/g, '') // Remove Next.js dynamic segments
3167
+ .replace(/:/g, '') // Remove React Router params
3168
+ .split(' ')
3169
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
3170
+ .join(' ')
3171
+ .trim() || 'Page';
3172
+ }
3173
+ /**
3174
+ * Check if route should be excluded based on glob patterns
3175
+ */
3176
+ shouldExcludeRoute(path, patterns) {
3177
+ if (!patterns?.length)
3178
+ return false;
3179
+ for (const pattern of patterns) {
3180
+ // Simple glob matching (supports * wildcard)
3181
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
3182
+ if (regex.test(path))
3183
+ return true;
3184
+ }
3185
+ return false;
3186
+ }
3187
+ /**
3188
+ * Force refresh from all sources
3189
+ */
3190
+ async refresh() {
3191
+ this.backendCache = null;
3192
+ this.isInitialized = false;
3193
+ this.initPromise = null;
3194
+ await this.initialize();
3195
+ }
3196
+ /**
3197
+ * Check if initialized
3198
+ */
3199
+ isReady() {
3200
+ return this.isInitialized;
3201
+ }
3202
+ /**
3203
+ * Get current entry counts by source
3204
+ */
3205
+ getStats() {
3206
+ return {
3207
+ static: this.staticEntries.length,
3208
+ discovered: this.discoveredEntries.length,
3209
+ backend: this.backendEntries.length,
3210
+ merged: this.getMergedEntries().length
3211
+ };
3212
+ }
3213
+ }
3214
+
2666
3215
  // src/agentic/CyberneticIntentClassifier.ts
2667
3216
  // Hybrid intent classification for agentic capabilities
2668
3217
  /**
@@ -2705,15 +3254,28 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2705
3254
  * a local DOM action or be sent to the backend RAG.
2706
3255
  */
2707
3256
  class CyberneticIntentClassifier {
2708
- constructor(config) {
3257
+ /**
3258
+ * Create a new intent classifier
3259
+ *
3260
+ * @param config - Agent configuration
3261
+ * @param apiUrl - Backend API URL (for multi-source sitemap)
3262
+ * @param apiKey - API key for authentication
3263
+ */
3264
+ constructor(config, apiUrl = '', apiKey = '') {
2709
3265
  this.categories = [];
2710
3266
  this.topicKeywords = new Map(); // topic -> keywords
3267
+ /** Multi-source sitemap discovery instance */
3268
+ this.siteMapDiscovery = null;
2711
3269
  this.config = config;
2712
3270
  this.siteMapIndex = new Map();
2713
3271
  this.formIndex = new Map();
2714
3272
  this.modalIndex = new Map();
2715
3273
  // Build indexes for fast lookup
2716
3274
  this.buildIndexes();
3275
+ // Initialize multi-source sitemap discovery if configured
3276
+ if (config.siteMapConfig) {
3277
+ this.siteMapDiscovery = new SiteMapDiscovery(config.siteMapConfig, apiUrl, apiKey);
3278
+ }
2717
3279
  }
2718
3280
  /**
2719
3281
  * Build search indexes from configuration
@@ -2984,22 +3546,35 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2984
3546
  }
2985
3547
  return '';
2986
3548
  }
3549
+ /**
3550
+ * Strip common articles and filler words for better matching
3551
+ */
3552
+ stripArticles(text) {
3553
+ return text
3554
+ .replace(/^(the|a|an|my|our|your|this|that)\s+/gi, '')
3555
+ .replace(/\s+(page|section|screen|view)$/gi, '')
3556
+ .trim();
3557
+ }
2987
3558
  /**
2988
3559
  * Find site map match for target string
2989
3560
  */
2990
3561
  findSiteMapMatch(target) {
2991
3562
  const normalizedTarget = target.toLowerCase().replace(/[^a-z0-9\s]/g, '');
2992
- // Exact match
2993
- const exact = this.siteMapIndex.get(normalizedTarget);
3563
+ const strippedTarget = this.stripArticles(normalizedTarget);
3564
+ // Exact match (try both with and without articles)
3565
+ const exact = this.siteMapIndex.get(normalizedTarget) || this.siteMapIndex.get(strippedTarget);
2994
3566
  if (exact) {
2995
3567
  return { entry: exact, similarity: 1.0 };
2996
3568
  }
2997
- // Fuzzy match
3569
+ // Fuzzy match using stripped version for better accuracy
2998
3570
  let bestMatch = null;
2999
3571
  let bestScore = 0;
3000
3572
  for (const [key, entry] of this.siteMapIndex) {
3001
- const score = this.calculateSimilarity(normalizedTarget, key);
3002
- if (score > bestScore && score > 0.6) {
3573
+ // Compare both original and stripped versions, take the better score
3574
+ const score1 = this.calculateSimilarity(normalizedTarget, key);
3575
+ const score2 = this.calculateSimilarity(strippedTarget, key);
3576
+ const score = Math.max(score1, score2);
3577
+ if (score > bestScore && score > 0.5) {
3003
3578
  bestScore = score;
3004
3579
  bestMatch = entry;
3005
3580
  }
@@ -3396,6 +3971,124 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
3396
3971
  isTrained() {
3397
3972
  return this.categories.length > 0 || this.topicKeywords.size > 0;
3398
3973
  }
3974
+ // ==================== MULTI-SOURCE SITEMAP DISCOVERY ====================
3975
+ /**
3976
+ * Initialize multi-source sitemap discovery
3977
+ * Call this after construction to load all sitemap sources
3978
+ *
3979
+ * @returns Promise that resolves when discovery is complete
3980
+ */
3981
+ async initializeDiscovery() {
3982
+ if (!this.siteMapDiscovery) {
3983
+ console.log('[CyberneticIntentClassifier] No siteMapConfig provided, skipping discovery');
3984
+ return;
3985
+ }
3986
+ try {
3987
+ await this.siteMapDiscovery.initialize();
3988
+ this.rebuildIndexesFromDiscovery();
3989
+ console.log(`[CyberneticIntentClassifier] Discovery complete, indexed ${this.siteMapIndex.size} entries`);
3990
+ }
3991
+ catch (error) {
3992
+ console.error('[CyberneticIntentClassifier] Discovery initialization failed:', error);
3993
+ }
3994
+ }
3995
+ /**
3996
+ * Rebuild sitemap indexes from discovery results
3997
+ * Merges discovered entries with existing static config
3998
+ */
3999
+ rebuildIndexesFromDiscovery() {
4000
+ if (!this.siteMapDiscovery)
4001
+ return;
4002
+ // Get merged entries from discovery (includes static props)
4003
+ const mergedEntries = this.siteMapDiscovery.getLegacyEntries();
4004
+ // Clear existing sitemap index (keep form and modal indexes)
4005
+ this.siteMapIndex.clear();
4006
+ // Index all merged entries
4007
+ for (const entry of mergedEntries) {
4008
+ const siteMapEntry = {
4009
+ path: entry.path,
4010
+ name: entry.name,
4011
+ description: entry.description,
4012
+ params: entry.params,
4013
+ aliases: entry.aliases
4014
+ };
4015
+ // Index by name and aliases
4016
+ const keys = [
4017
+ entry.name.toLowerCase(),
4018
+ entry.path.toLowerCase(),
4019
+ ...(entry.aliases || []).map(a => a.toLowerCase())
4020
+ ];
4021
+ // Also index description words for fuzzy matching
4022
+ if (entry.description) {
4023
+ const descWords = entry.description
4024
+ .toLowerCase()
4025
+ .split(/\s+/)
4026
+ .filter(w => w.length > 4 && !this.isStopWord(w));
4027
+ keys.push(...descWords);
4028
+ }
4029
+ for (const key of keys) {
4030
+ this.siteMapIndex.set(key, siteMapEntry);
4031
+ }
4032
+ }
4033
+ }
4034
+ /**
4035
+ * Check if sitemap discovery is ready
4036
+ *
4037
+ * @returns true if discovery is complete and entries are loaded
4038
+ */
4039
+ isSiteMapReady() {
4040
+ if (!this.siteMapDiscovery) {
4041
+ // No discovery configured - check if static sitemap exists
4042
+ return this.siteMapIndex.size > 0 || (this.config.siteMap?.length ?? 0) > 0;
4043
+ }
4044
+ return this.siteMapDiscovery.isReady();
4045
+ }
4046
+ /**
4047
+ * Ensure sitemap is ready before classification
4048
+ * Initializes discovery if not already done
4049
+ *
4050
+ * @returns Promise that resolves when sitemap is ready
4051
+ */
4052
+ async ensureSiteMapReady() {
4053
+ if (this.isSiteMapReady()) {
4054
+ return;
4055
+ }
4056
+ await this.initializeDiscovery();
4057
+ }
4058
+ /**
4059
+ * Refresh sitemap from all sources
4060
+ * Forces reload of backend and re-discovery
4061
+ *
4062
+ * @returns Promise that resolves when refresh is complete
4063
+ */
4064
+ async refreshSiteMap() {
4065
+ if (!this.siteMapDiscovery) {
4066
+ console.log('[CyberneticIntentClassifier] No siteMapConfig provided, cannot refresh');
4067
+ return;
4068
+ }
4069
+ await this.siteMapDiscovery.refresh();
4070
+ this.rebuildIndexesFromDiscovery();
4071
+ }
4072
+ /**
4073
+ * Get the SiteMapDiscovery instance for advanced use cases
4074
+ *
4075
+ * @returns The SiteMapDiscovery instance or null if not configured
4076
+ */
4077
+ getSiteMapDiscovery() {
4078
+ return this.siteMapDiscovery;
4079
+ }
4080
+ /**
4081
+ * Get current sitemap entries (merged from all sources)
4082
+ *
4083
+ * @returns Array of sitemap entries
4084
+ */
4085
+ getSiteMapEntries() {
4086
+ if (this.siteMapDiscovery) {
4087
+ return this.siteMapDiscovery.getLegacyEntries();
4088
+ }
4089
+ // Fall back to static config
4090
+ return this.config.siteMap || [];
4091
+ }
3399
4092
  }
3400
4093
 
3401
4094
  // src/agentic/CyberneticAgent.ts
@@ -4032,6 +4725,284 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
4032
4725
  client.registerAgentic(capabilities);
4033
4726
  }
4034
4727
 
4728
+ // src/hooks/useSiteMapDiscovery.ts
4729
+ // React hook for sitemap discovery integration
4730
+ // Note: React is a peer dependency - this hook is optional for non-React consumers
4731
+ /**
4732
+ * Hook for discovering routes from React Router and other frameworks
4733
+ *
4734
+ * @example
4735
+ * ```tsx
4736
+ * function App() {
4737
+ * const { entries, isDiscovering } = useSiteMapDiscovery({
4738
+ * enabled: true,
4739
+ * excludePaths: ['/admin/*', '/internal/*']
4740
+ * });
4741
+ *
4742
+ * return (
4743
+ * <ChatbotWidget
4744
+ * agent={{
4745
+ * enabled: true,
4746
+ * siteMap: entries
4747
+ * }}
4748
+ * />
4749
+ * );
4750
+ * }
4751
+ * ```
4752
+ */
4753
+ function useSiteMapDiscovery(options = {}) {
4754
+ const { enabled = true, transformRoute: _transformRoute, excludePaths: _excludePaths = [], navSelectors: _navSelectors, disableDomFallback = false } = options;
4755
+ const [state, setState] = react.useState({
4756
+ entries: [],
4757
+ isDiscovering: false,
4758
+ error: null,
4759
+ framework: null
4760
+ });
4761
+ const discoveredRef = react.useRef(false);
4762
+ const optionsRef = react.useRef(options);
4763
+ optionsRef.current = options;
4764
+ /**
4765
+ * Detect JavaScript framework
4766
+ */
4767
+ const detectFramework = react.useCallback(() => {
4768
+ if (typeof window === 'undefined')
4769
+ return 'generic';
4770
+ const win = window;
4771
+ if (win.__REACT_ROUTER_DATA_ROUTER__)
4772
+ return 'react-router';
4773
+ if (win.__reactRouterContext)
4774
+ return 'react-router';
4775
+ if (win.__VUE_ROUTER__ || win.__VUE_APP__?.$router)
4776
+ return 'vue-router';
4777
+ if (win.__NEXT_DATA__)
4778
+ return 'next';
4779
+ if (win.ng || document.querySelector('[ng-version]'))
4780
+ return 'angular';
4781
+ return 'generic';
4782
+ }, []);
4783
+ /**
4784
+ * Check if path should be excluded
4785
+ */
4786
+ const shouldExclude = react.useCallback((path) => {
4787
+ const patterns = optionsRef.current.excludePaths || [];
4788
+ for (const pattern of patterns) {
4789
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
4790
+ if (regex.test(path))
4791
+ return true;
4792
+ }
4793
+ return false;
4794
+ }, []);
4795
+ /**
4796
+ * Convert path to human-readable name
4797
+ */
4798
+ const pathToName = react.useCallback((path) => {
4799
+ const segments = path.split('/').filter(Boolean);
4800
+ const last = segments[segments.length - 1] || 'Home';
4801
+ return last
4802
+ .replace(/[-_:]/g, ' ')
4803
+ .replace(/\[.*?\]/g, '')
4804
+ .split(' ')
4805
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
4806
+ .join(' ')
4807
+ .trim() || 'Page';
4808
+ }, []);
4809
+ /**
4810
+ * Default route transform
4811
+ */
4812
+ const defaultTransform = react.useCallback((route) => {
4813
+ if (!route.path || route.path === '*' || route.path === '**')
4814
+ return null;
4815
+ return {
4816
+ path: route.path,
4817
+ name: route.name || route.meta?.title || pathToName(route.path),
4818
+ description: route.meta?.description,
4819
+ aliases: route.meta?.keywords || route.meta?.aliases,
4820
+ dynamicParams: route.path.includes(':') || route.path.includes('[')
4821
+ };
4822
+ }, [pathToName]);
4823
+ /**
4824
+ * Extract routes from React Router config
4825
+ */
4826
+ const extractReactRouterRoutes = react.useCallback((routeConfigs, routes, parentPath) => {
4827
+ for (const config of routeConfigs) {
4828
+ let path = config.path || '';
4829
+ if (path && !path.startsWith('/')) {
4830
+ path = parentPath ? `${parentPath}/${path}`.replace(/\/+/g, '/') : `/${path}`;
4831
+ }
4832
+ else if (!path && parentPath) {
4833
+ path = parentPath;
4834
+ }
4835
+ const route = {
4836
+ path,
4837
+ name: config.handle?.name || config.handle?.title || config.id,
4838
+ meta: config.handle || {}
4839
+ };
4840
+ if (config.path && !shouldExclude(path)) {
4841
+ routes.push(route);
4842
+ }
4843
+ if (config.children) {
4844
+ extractReactRouterRoutes(config.children, routes, path);
4845
+ }
4846
+ }
4847
+ }, [shouldExclude]);
4848
+ /**
4849
+ * Discover routes from DOM navigation
4850
+ */
4851
+ const discoverFromDOM = react.useCallback(() => {
4852
+ if (typeof document === 'undefined')
4853
+ return [];
4854
+ const defaultSelectors = [
4855
+ 'nav a[href]',
4856
+ '[role="navigation"] a[href]',
4857
+ '.nav a[href]',
4858
+ '.sidebar a[href]',
4859
+ '.menu a[href]',
4860
+ 'header a[href]'
4861
+ ];
4862
+ const selectors = optionsRef.current.navSelectors || defaultSelectors;
4863
+ const routes = [];
4864
+ const seen = new Set();
4865
+ for (const selector of selectors) {
4866
+ try {
4867
+ const links = document.querySelectorAll(selector);
4868
+ for (const link of links) {
4869
+ const href = link.getAttribute('href');
4870
+ if (!href || href.startsWith('http') || href.startsWith('javascript:'))
4871
+ continue;
4872
+ const path = href.startsWith('/') ? href : `/${href}`;
4873
+ const cleanPath = path.split('?')[0].split('#')[0];
4874
+ if (seen.has(cleanPath) || cleanPath === '/' || shouldExclude(cleanPath))
4875
+ continue;
4876
+ seen.add(cleanPath);
4877
+ routes.push({
4878
+ path: cleanPath,
4879
+ name: link.textContent?.trim() || pathToName(cleanPath)
4880
+ });
4881
+ }
4882
+ }
4883
+ catch (e) {
4884
+ // Invalid selector
4885
+ }
4886
+ }
4887
+ return routes;
4888
+ }, [shouldExclude, pathToName]);
4889
+ /**
4890
+ * Main discovery function
4891
+ */
4892
+ const discoverRoutes = react.useCallback(() => {
4893
+ if (!enabled || typeof window === 'undefined') {
4894
+ return;
4895
+ }
4896
+ setState((prev) => ({ ...prev, isDiscovering: true, error: null }));
4897
+ try {
4898
+ const framework = detectFramework();
4899
+ const routes = [];
4900
+ const win = window;
4901
+ // Try framework-specific discovery
4902
+ if (framework === 'react-router' && win.__REACT_ROUTER_DATA_ROUTER__?.routes) {
4903
+ extractReactRouterRoutes(win.__REACT_ROUTER_DATA_ROUTER__.routes, routes, '');
4904
+ }
4905
+ else if (framework === 'vue-router') {
4906
+ const router = win.__VUE_APP__?.$router || win.__VUE_ROUTER__;
4907
+ if (router?.getRoutes) {
4908
+ for (const vueRoute of router.getRoutes()) {
4909
+ if (!shouldExclude(vueRoute.path)) {
4910
+ routes.push({
4911
+ path: vueRoute.path,
4912
+ name: vueRoute.name?.toString(),
4913
+ meta: vueRoute.meta || {}
4914
+ });
4915
+ }
4916
+ }
4917
+ }
4918
+ }
4919
+ else if (framework === 'next' && win.__NEXT_DATA__?.buildManifest?.pages) {
4920
+ for (const pagePath of Object.keys(win.__NEXT_DATA__.buildManifest.pages)) {
4921
+ if (!pagePath.startsWith('/_') && !shouldExclude(pagePath)) {
4922
+ routes.push({
4923
+ path: pagePath,
4924
+ name: pathToName(pagePath)
4925
+ });
4926
+ }
4927
+ }
4928
+ }
4929
+ // DOM fallback if no routes found and not disabled
4930
+ if (routes.length === 0 && !disableDomFallback) {
4931
+ routes.push(...discoverFromDOM());
4932
+ }
4933
+ // Transform routes to sitemap entries
4934
+ const transform = optionsRef.current.transformRoute || defaultTransform;
4935
+ const entries = [];
4936
+ for (const route of routes) {
4937
+ const entry = transform(route);
4938
+ if (entry) {
4939
+ entries.push(entry);
4940
+ }
4941
+ }
4942
+ setState({
4943
+ entries,
4944
+ isDiscovering: false,
4945
+ error: null,
4946
+ framework
4947
+ });
4948
+ discoveredRef.current = true;
4949
+ console.log(`[useSiteMapDiscovery] Discovered ${entries.length} routes from ${framework}`);
4950
+ }
4951
+ catch (error) {
4952
+ console.warn('[useSiteMapDiscovery] Discovery failed:', error);
4953
+ setState((prev) => ({
4954
+ ...prev,
4955
+ isDiscovering: false,
4956
+ error: String(error)
4957
+ }));
4958
+ }
4959
+ }, [
4960
+ enabled,
4961
+ detectFramework,
4962
+ extractReactRouterRoutes,
4963
+ discoverFromDOM,
4964
+ defaultTransform,
4965
+ shouldExclude,
4966
+ pathToName,
4967
+ disableDomFallback
4968
+ ]);
4969
+ // Initial discovery with delay to ensure router is mounted
4970
+ react.useEffect(() => {
4971
+ if (!enabled || discoveredRef.current)
4972
+ return;
4973
+ const timer = setTimeout(discoverRoutes, 100);
4974
+ return () => clearTimeout(timer);
4975
+ }, [enabled, discoverRoutes]);
4976
+ // Re-discover on location changes (for SPAs)
4977
+ react.useEffect(() => {
4978
+ if (!enabled || typeof window === 'undefined')
4979
+ return;
4980
+ const handlePopState = () => {
4981
+ // Re-run discovery after navigation (routes might have changed)
4982
+ setTimeout(discoverRoutes, 50);
4983
+ };
4984
+ window.addEventListener('popstate', handlePopState);
4985
+ return () => window.removeEventListener('popstate', handlePopState);
4986
+ }, [enabled, discoverRoutes]);
4987
+ return {
4988
+ ...state,
4989
+ refresh: discoverRoutes
4990
+ };
4991
+ }
4992
+ /**
4993
+ * Create a FrameworkDiscoveryConfig from hook options
4994
+ * Useful for passing to CyberneticClient
4995
+ */
4996
+ function createDiscoveryConfig(options) {
4997
+ return {
4998
+ enabled: options.enabled ?? true,
4999
+ domFallback: !options.disableDomFallback,
5000
+ navSelectors: options.navSelectors,
5001
+ excludePaths: options.excludePaths,
5002
+ transformRoute: options.transformRoute
5003
+ };
5004
+ }
5005
+
4035
5006
  // © 2026 AsterMind AI Co. – All Rights Reserved.
4036
5007
  // Patent Pending US 63/897,713
4037
5008
  // ELMConfig.ts - Configuration interfaces, defaults, helpers for ELM-based models
@@ -5437,8 +6408,10 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
5437
6408
  exports.LicenseManager = LicenseManager;
5438
6409
  exports.OmegaOfflineRAG = OmegaOfflineRAG;
5439
6410
  exports.REQUIRED_FEATURES = REQUIRED_FEATURES;
6411
+ exports.SiteMapDiscovery = SiteMapDiscovery;
5440
6412
  exports.configLoaders = configLoaders;
5441
6413
  exports.createClient = createClient;
6414
+ exports.createDiscoveryConfig = createDiscoveryConfig;
5442
6415
  exports.createLicenseManager = createLicenseManager;
5443
6416
  exports.detectEnvironment = detectEnvironment;
5444
6417
  exports.getEnforcementMode = getEnforcementMode;
@@ -5446,6 +6419,7 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
5446
6419
  exports.isValidJWTFormat = isValidJWTFormat;
5447
6420
  exports.loadConfig = loadConfig;
5448
6421
  exports.registerAgenticCapabilities = registerAgenticCapabilities;
6422
+ exports.useSiteMapDiscovery = useSiteMapDiscovery;
5449
6423
  exports.validateConfig = validateConfig;
5450
6424
  exports.verifyLicenseToken = verifyLicenseToken;
5451
6425