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