@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.
@@ -2654,6 +2654,555 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2654
2654
  }
2655
2655
  }
2656
2656
 
2657
+ // src/agentic/SiteMapDiscovery.ts
2658
+ // Multi-source sitemap discovery and merging for agentic navigation
2659
+ /**
2660
+ * Multi-source sitemap discovery and management
2661
+ *
2662
+ * Combines routes from three sources:
2663
+ * 1. Static props (explicit configuration)
2664
+ * 2. Framework auto-discovery (React Router, Vue Router, etc.)
2665
+ * 3. Backend API (tenant-specific sitemaps)
2666
+ */
2667
+ class SiteMapDiscovery {
2668
+ constructor(config, apiUrl, apiKey) {
2669
+ this.staticEntries = [];
2670
+ this.discoveredEntries = [];
2671
+ this.backendEntries = [];
2672
+ this.backendCache = null;
2673
+ this.isInitialized = false;
2674
+ this.initPromise = null;
2675
+ this.config = config;
2676
+ this.apiUrl = apiUrl;
2677
+ this.apiKey = apiKey;
2678
+ // Load static entries immediately (synchronous)
2679
+ if (config.static) {
2680
+ this.staticEntries = this.enhanceEntries(config.static, 'props');
2681
+ }
2682
+ }
2683
+ /**
2684
+ * Initialize all sitemap sources
2685
+ * Call this during widget initialization or first query based on loadStrategy
2686
+ */
2687
+ async initialize() {
2688
+ if (this.isInitialized)
2689
+ return;
2690
+ if (this.initPromise)
2691
+ return this.initPromise;
2692
+ this.initPromise = this.performInitialization();
2693
+ await this.initPromise;
2694
+ this.isInitialized = true;
2695
+ }
2696
+ async performInitialization() {
2697
+ const tasks = [];
2698
+ // Source 2: Framework auto-discovery
2699
+ if (this.config.discovery?.enabled !== false) {
2700
+ tasks.push(this.discoverFromFramework());
2701
+ }
2702
+ // Source 3: Backend API
2703
+ if (this.config.backend?.enabled !== false && this.config.backend?.endpoint) {
2704
+ tasks.push(this.fetchFromBackend());
2705
+ }
2706
+ const results = await Promise.allSettled(tasks);
2707
+ for (const result of results) {
2708
+ if (result.status === 'fulfilled') {
2709
+ const { entries, source } = result.value;
2710
+ if (source === 'discovery') {
2711
+ this.discoveredEntries = entries;
2712
+ }
2713
+ else if (source === 'backend') {
2714
+ this.backendEntries = entries;
2715
+ }
2716
+ }
2717
+ else {
2718
+ console.warn('[SiteMapDiscovery] Source failed:', result.reason);
2719
+ }
2720
+ }
2721
+ const total = this.staticEntries.length + this.discoveredEntries.length + this.backendEntries.length;
2722
+ console.log(`[SiteMapDiscovery] Initialized with ${total} total entries (${this.staticEntries.length} static, ${this.discoveredEntries.length} discovered, ${this.backendEntries.length} backend)`);
2723
+ }
2724
+ /**
2725
+ * Get merged sitemap entries with deduplication
2726
+ */
2727
+ getMergedEntries() {
2728
+ return this.mergeEntries(this.staticEntries, this.discoveredEntries, this.backendEntries);
2729
+ }
2730
+ /**
2731
+ * Convert to legacy AgenticSiteMapEntry format for classifier compatibility
2732
+ */
2733
+ getLegacyEntries() {
2734
+ return this.getMergedEntries().map(e => ({
2735
+ path: e.path,
2736
+ name: e.name,
2737
+ description: e.description,
2738
+ params: e.params,
2739
+ dynamicParams: e.dynamicParams,
2740
+ aliases: e.aliases
2741
+ }));
2742
+ }
2743
+ // ==================== SOURCE 2: FRAMEWORK DISCOVERY ====================
2744
+ async discoverFromFramework() {
2745
+ const config = this.config.discovery || {};
2746
+ const entries = [];
2747
+ try {
2748
+ // Detect framework if not specified
2749
+ const framework = config.framework || this.detectFramework();
2750
+ let routes = [];
2751
+ switch (framework) {
2752
+ case 'react-router':
2753
+ routes = this.discoverReactRouter();
2754
+ break;
2755
+ case 'vue-router':
2756
+ routes = this.discoverVueRouter();
2757
+ break;
2758
+ case 'next':
2759
+ routes = this.discoverNextRoutes();
2760
+ break;
2761
+ case 'angular':
2762
+ routes = this.discoverAngularRoutes();
2763
+ break;
2764
+ default:
2765
+ if (config.domFallback !== false) {
2766
+ routes = this.discoverFromDOM(config.navSelectors);
2767
+ }
2768
+ }
2769
+ // Transform routes to sitemap entries
2770
+ for (const route of routes) {
2771
+ // Apply exclude patterns
2772
+ if (this.shouldExcludeRoute(route.path, config.excludePaths)) {
2773
+ continue;
2774
+ }
2775
+ // Apply custom transform if provided
2776
+ let entry;
2777
+ if (config.transformRoute) {
2778
+ entry = config.transformRoute(route);
2779
+ }
2780
+ else {
2781
+ entry = this.defaultRouteTransform(route);
2782
+ }
2783
+ if (entry) {
2784
+ entries.push(this.enhanceEntry(entry, 'discovery'));
2785
+ }
2786
+ }
2787
+ console.log(`[SiteMapDiscovery] Discovered ${entries.length} routes from ${framework}`);
2788
+ }
2789
+ catch (error) {
2790
+ console.warn('[SiteMapDiscovery] Framework discovery failed:', error);
2791
+ return {
2792
+ entries: [],
2793
+ source: 'discovery',
2794
+ timestamp: Date.now(),
2795
+ error: String(error)
2796
+ };
2797
+ }
2798
+ return {
2799
+ entries,
2800
+ source: 'discovery',
2801
+ timestamp: Date.now()
2802
+ };
2803
+ }
2804
+ /**
2805
+ * Detect JavaScript framework from global objects
2806
+ */
2807
+ detectFramework() {
2808
+ if (typeof window === 'undefined')
2809
+ return 'generic';
2810
+ const win = window;
2811
+ // React Router v6.4+ data router
2812
+ if (win.__REACT_ROUTER_DATA_ROUTER__) {
2813
+ return 'react-router';
2814
+ }
2815
+ // React Router v5 context
2816
+ if (win.__reactRouterContext) {
2817
+ return 'react-router';
2818
+ }
2819
+ // Vue Router
2820
+ if (win.__VUE_ROUTER__ || win.__VUE_APP__?.$router) {
2821
+ return 'vue-router';
2822
+ }
2823
+ // Next.js
2824
+ if (win.__NEXT_DATA__) {
2825
+ return 'next';
2826
+ }
2827
+ // Angular
2828
+ if (win.ng || document.querySelector('[ng-version]')) {
2829
+ return 'angular';
2830
+ }
2831
+ return 'generic';
2832
+ }
2833
+ /**
2834
+ * Discover routes from React Router
2835
+ */
2836
+ discoverReactRouter() {
2837
+ const routes = [];
2838
+ if (typeof window === 'undefined')
2839
+ return routes;
2840
+ const win = window;
2841
+ // Try React Router v6.4+ data router
2842
+ const dataRouter = win.__REACT_ROUTER_DATA_ROUTER__;
2843
+ if (dataRouter?.routes) {
2844
+ this.extractRoutesFromConfig(dataRouter.routes, routes, '');
2845
+ return routes;
2846
+ }
2847
+ // Fallback: Try to find routes from __REACT_DEVTOOLS_GLOBAL_HOOK__
2848
+ // This is less reliable but can work in some cases
2849
+ return routes;
2850
+ }
2851
+ /**
2852
+ * Extract routes recursively from React Router config
2853
+ */
2854
+ extractRoutesFromConfig(routeConfigs, routes, parentPath) {
2855
+ for (const config of routeConfigs) {
2856
+ const path = this.normalizePath(parentPath, config.path || '');
2857
+ // Extract route info from handle metadata
2858
+ const route = {
2859
+ path,
2860
+ name: config.handle?.name || config.handle?.title || config.id,
2861
+ meta: config.handle || {},
2862
+ children: []
2863
+ };
2864
+ // Only include routes with paths (skip layout routes without path)
2865
+ if (config.path || config.index) {
2866
+ routes.push(route);
2867
+ }
2868
+ // Recursively process children
2869
+ if (config.children) {
2870
+ this.extractRoutesFromConfig(config.children, routes, path);
2871
+ }
2872
+ }
2873
+ }
2874
+ /**
2875
+ * Discover routes from Vue Router
2876
+ */
2877
+ discoverVueRouter() {
2878
+ const routes = [];
2879
+ if (typeof window === 'undefined')
2880
+ return routes;
2881
+ const win = window;
2882
+ // Access Vue Router instance
2883
+ const app = win.__VUE_APP__;
2884
+ const router = app?.$router || win.__VUE_ROUTER__;
2885
+ if (router?.getRoutes) {
2886
+ const vueRoutes = router.getRoutes();
2887
+ for (const vueRoute of vueRoutes) {
2888
+ routes.push({
2889
+ path: vueRoute.path,
2890
+ name: vueRoute.name?.toString(),
2891
+ meta: vueRoute.meta || {},
2892
+ component: vueRoute.components?.default?.name
2893
+ });
2894
+ }
2895
+ }
2896
+ return routes;
2897
+ }
2898
+ /**
2899
+ * Discover routes from Next.js
2900
+ */
2901
+ discoverNextRoutes() {
2902
+ const routes = [];
2903
+ if (typeof window === 'undefined')
2904
+ return routes;
2905
+ const win = window;
2906
+ const nextData = win.__NEXT_DATA__;
2907
+ if (!nextData)
2908
+ return routes;
2909
+ // Next.js doesn't expose routes directly in client
2910
+ // We can extract from build manifest if available
2911
+ const pages = nextData.buildManifest?.pages || {};
2912
+ for (const pagePath of Object.keys(pages)) {
2913
+ if (pagePath.startsWith('/_'))
2914
+ continue; // Skip internal pages
2915
+ routes.push({
2916
+ path: pagePath,
2917
+ name: this.pathToName(pagePath)
2918
+ });
2919
+ }
2920
+ return routes;
2921
+ }
2922
+ /**
2923
+ * Discover routes from Angular Router
2924
+ */
2925
+ discoverAngularRoutes() {
2926
+ // Angular route discovery requires access to Router service
2927
+ // which is typically not exposed globally
2928
+ // Users should provide routes via props or backend
2929
+ return [];
2930
+ }
2931
+ /**
2932
+ * Discover routes by scanning DOM navigation elements
2933
+ */
2934
+ discoverFromDOM(selectors) {
2935
+ const routes = [];
2936
+ if (typeof document === 'undefined')
2937
+ 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;
2948
+ const seen = new Set();
2949
+ for (const selector of allSelectors) {
2950
+ try {
2951
+ const links = document.querySelectorAll(selector);
2952
+ for (const link of links) {
2953
+ const href = link.getAttribute('href');
2954
+ if (!href)
2955
+ continue;
2956
+ // Only process internal links
2957
+ if (href.startsWith('http') && !href.startsWith(window.location.origin)) {
2958
+ continue;
2959
+ }
2960
+ // 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;
2965
+ }
2966
+ seen.add(path);
2967
+ // Clean path (remove query string and hash)
2968
+ const cleanPath = path.split('?')[0].split('#')[0];
2969
+ routes.push({
2970
+ path: cleanPath,
2971
+ name: link.textContent?.trim() || this.pathToName(cleanPath)
2972
+ });
2973
+ }
2974
+ }
2975
+ catch (e) {
2976
+ // Invalid selector - skip
2977
+ }
2978
+ }
2979
+ return routes;
2980
+ }
2981
+ // ==================== SOURCE 3: BACKEND API ====================
2982
+ async fetchFromBackend() {
2983
+ const config = this.config.backend || {};
2984
+ const endpoint = config.endpoint || '/api/external/sitemap';
2985
+ // Check cache
2986
+ if (this.backendCache && Date.now() < this.backendCache.expiresAt) {
2987
+ return {
2988
+ entries: this.backendCache.entries,
2989
+ source: 'backend',
2990
+ timestamp: this.backendCache.fetchedAt
2991
+ };
2992
+ }
2993
+ try {
2994
+ const url = new URL(endpoint, this.apiUrl);
2995
+ // Add query params
2996
+ if (config.tenantScoped !== false) {
2997
+ url.searchParams.set('tenantScoped', 'true');
2998
+ }
2999
+ if (config.includeGeneral !== false) {
3000
+ url.searchParams.set('includeGeneral', 'true');
3001
+ }
3002
+ const response = await fetch(url.toString(), {
3003
+ method: 'GET',
3004
+ headers: {
3005
+ 'X-API-Key': this.apiKey,
3006
+ 'Content-Type': 'application/json',
3007
+ ...config.headers
3008
+ }
3009
+ });
3010
+ if (!response.ok) {
3011
+ throw new Error(`Backend sitemap fetch failed: ${response.status}`);
3012
+ }
3013
+ const data = await response.json();
3014
+ const entries = this.transformBackendResponse(data);
3015
+ // Update cache
3016
+ const ttl = config.cacheTtl || 300000; // 5 minutes default
3017
+ this.backendCache = {
3018
+ entries,
3019
+ fetchedAt: Date.now(),
3020
+ expiresAt: Date.now() + ttl
3021
+ };
3022
+ console.log(`[SiteMapDiscovery] Loaded ${entries.length} routes from backend`);
3023
+ return {
3024
+ entries,
3025
+ source: 'backend',
3026
+ timestamp: Date.now()
3027
+ };
3028
+ }
3029
+ catch (error) {
3030
+ console.warn('[SiteMapDiscovery] Backend fetch failed:', error);
3031
+ return {
3032
+ entries: this.backendCache?.entries || [],
3033
+ source: 'backend',
3034
+ timestamp: Date.now(),
3035
+ error: String(error)
3036
+ };
3037
+ }
3038
+ }
3039
+ /**
3040
+ * Transform backend response to enhanced entries
3041
+ */
3042
+ transformBackendResponse(data) {
3043
+ // Handle array or object with entries/sitemap key
3044
+ const rawEntries = Array.isArray(data)
3045
+ ? data
3046
+ : data.entries || data.sitemap || data.routes || [];
3047
+ return rawEntries.map((entry) => this.enhanceEntry({
3048
+ path: entry.url || entry.path,
3049
+ name: entry.title || entry.name,
3050
+ description: entry.description,
3051
+ aliases: entry.keywords || entry.aliases,
3052
+ params: entry.params,
3053
+ dynamicParams: entry.dynamicParams
3054
+ }, 'backend'));
3055
+ }
3056
+ // ==================== MERGE LOGIC ====================
3057
+ /**
3058
+ * Merge entries from all sources with deduplication
3059
+ */
3060
+ mergeEntries(...sources) {
3061
+ const mergeConfig = this.config.merge || {};
3062
+ const priorityOrder = mergeConfig.sourcePriority || ['props', 'backend', 'discovery'];
3063
+ const dedupeStrategy = mergeConfig.deduplication || 'path';
3064
+ // Create priority map (higher number = higher priority)
3065
+ const priorityMap = new Map();
3066
+ priorityOrder.forEach((source, index) => {
3067
+ priorityMap.set(source, priorityOrder.length - index);
3068
+ });
3069
+ // Flatten all entries with priority scores
3070
+ const allEntries = [];
3071
+ for (const sourceEntries of sources) {
3072
+ for (const entry of sourceEntries) {
3073
+ const priority = priorityMap.get(entry._source || 'props') || 0;
3074
+ allEntries.push({ ...entry, _priority: priority });
3075
+ }
3076
+ }
3077
+ // Sort by priority (higher first)
3078
+ allEntries.sort((a, b) => (b._priority || 0) - (a._priority || 0));
3079
+ // Deduplicate
3080
+ const seen = new Map();
3081
+ for (const entry of allEntries) {
3082
+ let key;
3083
+ switch (dedupeStrategy) {
3084
+ case 'name':
3085
+ key = entry.name.toLowerCase();
3086
+ break;
3087
+ case 'both':
3088
+ key = `${entry.path}|${entry.name.toLowerCase()}`;
3089
+ break;
3090
+ case 'path':
3091
+ default:
3092
+ key = this.normalizePath('', entry.path);
3093
+ }
3094
+ if (!seen.has(key)) {
3095
+ seen.set(key, entry);
3096
+ }
3097
+ else if (mergeConfig.keepNonConflicting !== false) {
3098
+ // Merge non-conflicting fields from lower priority entry
3099
+ const existing = seen.get(key);
3100
+ if (!existing.description && entry.description) {
3101
+ existing.description = entry.description;
3102
+ }
3103
+ if (!existing.aliases?.length && entry.aliases?.length) {
3104
+ existing.aliases = entry.aliases;
3105
+ }
3106
+ }
3107
+ }
3108
+ return Array.from(seen.values());
3109
+ }
3110
+ // ==================== HELPERS ====================
3111
+ enhanceEntries(entries, source) {
3112
+ return entries.map(e => this.enhanceEntry(e, source));
3113
+ }
3114
+ enhanceEntry(entry, source) {
3115
+ return {
3116
+ ...entry,
3117
+ _source: source,
3118
+ _discoveredAt: Date.now()
3119
+ };
3120
+ }
3121
+ /**
3122
+ * Default transformation from DiscoveredRoute to AgenticSiteMapEntry
3123
+ */
3124
+ defaultRouteTransform(route) {
3125
+ // Skip routes without meaningful paths
3126
+ if (!route.path || route.path === '*' || route.path === '**') {
3127
+ return null;
3128
+ }
3129
+ return {
3130
+ path: route.path,
3131
+ name: route.name || route.meta?.title || this.pathToName(route.path),
3132
+ description: route.meta?.description,
3133
+ aliases: route.meta?.keywords || route.meta?.aliases,
3134
+ dynamicParams: route.path.includes(':') || route.path.includes('[')
3135
+ };
3136
+ }
3137
+ /**
3138
+ * Normalize path by combining parent and child paths
3139
+ */
3140
+ normalizePath(parent, path) {
3141
+ if (!path || path === '/')
3142
+ return parent || '/';
3143
+ if (path.startsWith('/'))
3144
+ return path;
3145
+ if (!parent || parent === '/')
3146
+ return `/${path}`;
3147
+ return `${parent}/${path}`.replace(/\/+/g, '/');
3148
+ }
3149
+ /**
3150
+ * Convert path to human-readable name
3151
+ */
3152
+ pathToName(path) {
3153
+ const segments = path.split('/').filter(Boolean);
3154
+ const last = segments[segments.length - 1] || 'Home';
3155
+ return last
3156
+ .replace(/[-_]/g, ' ')
3157
+ .replace(/\[.*?\]/g, '') // Remove Next.js dynamic segments
3158
+ .replace(/:/g, '') // Remove React Router params
3159
+ .split(' ')
3160
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
3161
+ .join(' ')
3162
+ .trim() || 'Page';
3163
+ }
3164
+ /**
3165
+ * Check if route should be excluded based on glob patterns
3166
+ */
3167
+ shouldExcludeRoute(path, patterns) {
3168
+ if (!patterns?.length)
3169
+ return false;
3170
+ for (const pattern of patterns) {
3171
+ // Simple glob matching (supports * wildcard)
3172
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
3173
+ if (regex.test(path))
3174
+ return true;
3175
+ }
3176
+ return false;
3177
+ }
3178
+ /**
3179
+ * Force refresh from all sources
3180
+ */
3181
+ async refresh() {
3182
+ this.backendCache = null;
3183
+ this.isInitialized = false;
3184
+ this.initPromise = null;
3185
+ await this.initialize();
3186
+ }
3187
+ /**
3188
+ * Check if initialized
3189
+ */
3190
+ isReady() {
3191
+ return this.isInitialized;
3192
+ }
3193
+ /**
3194
+ * Get current entry counts by source
3195
+ */
3196
+ getStats() {
3197
+ return {
3198
+ static: this.staticEntries.length,
3199
+ discovered: this.discoveredEntries.length,
3200
+ backend: this.backendEntries.length,
3201
+ merged: this.getMergedEntries().length
3202
+ };
3203
+ }
3204
+ }
3205
+
2657
3206
  // src/agentic/CyberneticIntentClassifier.ts
2658
3207
  // Hybrid intent classification for agentic capabilities
2659
3208
  /**
@@ -2696,15 +3245,28 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2696
3245
  * a local DOM action or be sent to the backend RAG.
2697
3246
  */
2698
3247
  class CyberneticIntentClassifier {
2699
- constructor(config) {
3248
+ /**
3249
+ * Create a new intent classifier
3250
+ *
3251
+ * @param config - Agent configuration
3252
+ * @param apiUrl - Backend API URL (for multi-source sitemap)
3253
+ * @param apiKey - API key for authentication
3254
+ */
3255
+ constructor(config, apiUrl = '', apiKey = '') {
2700
3256
  this.categories = [];
2701
3257
  this.topicKeywords = new Map(); // topic -> keywords
3258
+ /** Multi-source sitemap discovery instance */
3259
+ this.siteMapDiscovery = null;
2702
3260
  this.config = config;
2703
3261
  this.siteMapIndex = new Map();
2704
3262
  this.formIndex = new Map();
2705
3263
  this.modalIndex = new Map();
2706
3264
  // Build indexes for fast lookup
2707
3265
  this.buildIndexes();
3266
+ // Initialize multi-source sitemap discovery if configured
3267
+ if (config.siteMapConfig) {
3268
+ this.siteMapDiscovery = new SiteMapDiscovery(config.siteMapConfig, apiUrl, apiKey);
3269
+ }
2708
3270
  }
2709
3271
  /**
2710
3272
  * Build search indexes from configuration
@@ -2975,22 +3537,35 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2975
3537
  }
2976
3538
  return '';
2977
3539
  }
3540
+ /**
3541
+ * Strip common articles and filler words for better matching
3542
+ */
3543
+ stripArticles(text) {
3544
+ return text
3545
+ .replace(/^(the|a|an|my|our|your|this|that)\s+/gi, '')
3546
+ .replace(/\s+(page|section|screen|view)$/gi, '')
3547
+ .trim();
3548
+ }
2978
3549
  /**
2979
3550
  * Find site map match for target string
2980
3551
  */
2981
3552
  findSiteMapMatch(target) {
2982
3553
  const normalizedTarget = target.toLowerCase().replace(/[^a-z0-9\s]/g, '');
2983
- // Exact match
2984
- const exact = this.siteMapIndex.get(normalizedTarget);
3554
+ const strippedTarget = this.stripArticles(normalizedTarget);
3555
+ // Exact match (try both with and without articles)
3556
+ const exact = this.siteMapIndex.get(normalizedTarget) || this.siteMapIndex.get(strippedTarget);
2985
3557
  if (exact) {
2986
3558
  return { entry: exact, similarity: 1.0 };
2987
3559
  }
2988
- // Fuzzy match
3560
+ // Fuzzy match using stripped version for better accuracy
2989
3561
  let bestMatch = null;
2990
3562
  let bestScore = 0;
2991
3563
  for (const [key, entry] of this.siteMapIndex) {
2992
- const score = this.calculateSimilarity(normalizedTarget, key);
2993
- if (score > bestScore && score > 0.6) {
3564
+ // Compare both original and stripped versions, take the better score
3565
+ const score1 = this.calculateSimilarity(normalizedTarget, key);
3566
+ const score2 = this.calculateSimilarity(strippedTarget, key);
3567
+ const score = Math.max(score1, score2);
3568
+ if (score > bestScore && score > 0.5) {
2994
3569
  bestScore = score;
2995
3570
  bestMatch = entry;
2996
3571
  }
@@ -3387,6 +3962,124 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
3387
3962
  isTrained() {
3388
3963
  return this.categories.length > 0 || this.topicKeywords.size > 0;
3389
3964
  }
3965
+ // ==================== MULTI-SOURCE SITEMAP DISCOVERY ====================
3966
+ /**
3967
+ * Initialize multi-source sitemap discovery
3968
+ * Call this after construction to load all sitemap sources
3969
+ *
3970
+ * @returns Promise that resolves when discovery is complete
3971
+ */
3972
+ async initializeDiscovery() {
3973
+ if (!this.siteMapDiscovery) {
3974
+ console.log('[CyberneticIntentClassifier] No siteMapConfig provided, skipping discovery');
3975
+ return;
3976
+ }
3977
+ try {
3978
+ await this.siteMapDiscovery.initialize();
3979
+ this.rebuildIndexesFromDiscovery();
3980
+ console.log(`[CyberneticIntentClassifier] Discovery complete, indexed ${this.siteMapIndex.size} entries`);
3981
+ }
3982
+ catch (error) {
3983
+ console.error('[CyberneticIntentClassifier] Discovery initialization failed:', error);
3984
+ }
3985
+ }
3986
+ /**
3987
+ * Rebuild sitemap indexes from discovery results
3988
+ * Merges discovered entries with existing static config
3989
+ */
3990
+ rebuildIndexesFromDiscovery() {
3991
+ if (!this.siteMapDiscovery)
3992
+ return;
3993
+ // Get merged entries from discovery (includes static props)
3994
+ const mergedEntries = this.siteMapDiscovery.getLegacyEntries();
3995
+ // Clear existing sitemap index (keep form and modal indexes)
3996
+ this.siteMapIndex.clear();
3997
+ // Index all merged entries
3998
+ for (const entry of mergedEntries) {
3999
+ const siteMapEntry = {
4000
+ path: entry.path,
4001
+ name: entry.name,
4002
+ description: entry.description,
4003
+ params: entry.params,
4004
+ aliases: entry.aliases
4005
+ };
4006
+ // Index by name and aliases
4007
+ const keys = [
4008
+ entry.name.toLowerCase(),
4009
+ entry.path.toLowerCase(),
4010
+ ...(entry.aliases || []).map(a => a.toLowerCase())
4011
+ ];
4012
+ // Also index description words for fuzzy matching
4013
+ if (entry.description) {
4014
+ const descWords = entry.description
4015
+ .toLowerCase()
4016
+ .split(/\s+/)
4017
+ .filter(w => w.length > 4 && !this.isStopWord(w));
4018
+ keys.push(...descWords);
4019
+ }
4020
+ for (const key of keys) {
4021
+ this.siteMapIndex.set(key, siteMapEntry);
4022
+ }
4023
+ }
4024
+ }
4025
+ /**
4026
+ * Check if sitemap discovery is ready
4027
+ *
4028
+ * @returns true if discovery is complete and entries are loaded
4029
+ */
4030
+ isSiteMapReady() {
4031
+ if (!this.siteMapDiscovery) {
4032
+ // No discovery configured - check if static sitemap exists
4033
+ return this.siteMapIndex.size > 0 || (this.config.siteMap?.length ?? 0) > 0;
4034
+ }
4035
+ return this.siteMapDiscovery.isReady();
4036
+ }
4037
+ /**
4038
+ * Ensure sitemap is ready before classification
4039
+ * Initializes discovery if not already done
4040
+ *
4041
+ * @returns Promise that resolves when sitemap is ready
4042
+ */
4043
+ async ensureSiteMapReady() {
4044
+ if (this.isSiteMapReady()) {
4045
+ return;
4046
+ }
4047
+ await this.initializeDiscovery();
4048
+ }
4049
+ /**
4050
+ * Refresh sitemap from all sources
4051
+ * Forces reload of backend and re-discovery
4052
+ *
4053
+ * @returns Promise that resolves when refresh is complete
4054
+ */
4055
+ async refreshSiteMap() {
4056
+ if (!this.siteMapDiscovery) {
4057
+ console.log('[CyberneticIntentClassifier] No siteMapConfig provided, cannot refresh');
4058
+ return;
4059
+ }
4060
+ await this.siteMapDiscovery.refresh();
4061
+ this.rebuildIndexesFromDiscovery();
4062
+ }
4063
+ /**
4064
+ * Get the SiteMapDiscovery instance for advanced use cases
4065
+ *
4066
+ * @returns The SiteMapDiscovery instance or null if not configured
4067
+ */
4068
+ getSiteMapDiscovery() {
4069
+ return this.siteMapDiscovery;
4070
+ }
4071
+ /**
4072
+ * Get current sitemap entries (merged from all sources)
4073
+ *
4074
+ * @returns Array of sitemap entries
4075
+ */
4076
+ getSiteMapEntries() {
4077
+ if (this.siteMapDiscovery) {
4078
+ return this.siteMapDiscovery.getLegacyEntries();
4079
+ }
4080
+ // Fall back to static config
4081
+ return this.config.siteMap || [];
4082
+ }
3390
4083
  }
3391
4084
 
3392
4085
  // src/agentic/CyberneticAgent.ts