@elliotding/ai-agent-mcp 0.1.0

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.
Files changed (191) hide show
  1. package/dist/api/cached-client.d.ts +48 -0
  2. package/dist/api/cached-client.d.ts.map +1 -0
  3. package/dist/api/cached-client.js +126 -0
  4. package/dist/api/cached-client.js.map +1 -0
  5. package/dist/api/client.d.ts +213 -0
  6. package/dist/api/client.d.ts.map +1 -0
  7. package/dist/api/client.js +326 -0
  8. package/dist/api/client.js.map +1 -0
  9. package/dist/auth/index.d.ts +8 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +26 -0
  12. package/dist/auth/index.js.map +1 -0
  13. package/dist/auth/middleware.d.ts +36 -0
  14. package/dist/auth/middleware.d.ts.map +1 -0
  15. package/dist/auth/middleware.js +194 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/permissions.d.ts +60 -0
  18. package/dist/auth/permissions.d.ts.map +1 -0
  19. package/dist/auth/permissions.js +256 -0
  20. package/dist/auth/permissions.js.map +1 -0
  21. package/dist/auth/token-validator.d.ts +52 -0
  22. package/dist/auth/token-validator.d.ts.map +1 -0
  23. package/dist/auth/token-validator.js +217 -0
  24. package/dist/auth/token-validator.js.map +1 -0
  25. package/dist/cache/cache-manager.d.ts +49 -0
  26. package/dist/cache/cache-manager.d.ts.map +1 -0
  27. package/dist/cache/cache-manager.js +191 -0
  28. package/dist/cache/cache-manager.js.map +1 -0
  29. package/dist/cache/index.d.ts +6 -0
  30. package/dist/cache/index.d.ts.map +1 -0
  31. package/dist/cache/index.js +12 -0
  32. package/dist/cache/index.js.map +1 -0
  33. package/dist/cache/redis-client.d.ts +45 -0
  34. package/dist/cache/redis-client.d.ts.map +1 -0
  35. package/dist/cache/redis-client.js +210 -0
  36. package/dist/cache/redis-client.js.map +1 -0
  37. package/dist/config/constants.d.ts +28 -0
  38. package/dist/config/constants.d.ts.map +1 -0
  39. package/dist/config/constants.js +31 -0
  40. package/dist/config/constants.js.map +1 -0
  41. package/dist/config/index.d.ts +54 -0
  42. package/dist/config/index.d.ts.map +1 -0
  43. package/dist/config/index.js +168 -0
  44. package/dist/config/index.js.map +1 -0
  45. package/dist/filesystem/manager.d.ts +45 -0
  46. package/dist/filesystem/manager.d.ts.map +1 -0
  47. package/dist/filesystem/manager.js +246 -0
  48. package/dist/filesystem/manager.js.map +1 -0
  49. package/dist/git/multi-source-manager.d.ts +62 -0
  50. package/dist/git/multi-source-manager.d.ts.map +1 -0
  51. package/dist/git/multi-source-manager.js +293 -0
  52. package/dist/git/multi-source-manager.js.map +1 -0
  53. package/dist/git/operations.d.ts +27 -0
  54. package/dist/git/operations.d.ts.map +1 -0
  55. package/dist/git/operations.js +83 -0
  56. package/dist/git/operations.js.map +1 -0
  57. package/dist/index.d.ts +6 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +109 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/monitoring/health.d.ts +35 -0
  62. package/dist/monitoring/health.d.ts.map +1 -0
  63. package/dist/monitoring/health.js +105 -0
  64. package/dist/monitoring/health.js.map +1 -0
  65. package/dist/resources/index.d.ts +6 -0
  66. package/dist/resources/index.d.ts.map +1 -0
  67. package/dist/resources/index.js +10 -0
  68. package/dist/resources/index.js.map +1 -0
  69. package/dist/resources/loader.d.ts +87 -0
  70. package/dist/resources/loader.d.ts.map +1 -0
  71. package/dist/resources/loader.js +452 -0
  72. package/dist/resources/loader.js.map +1 -0
  73. package/dist/server/http.d.ts +57 -0
  74. package/dist/server/http.d.ts.map +1 -0
  75. package/dist/server/http.js +336 -0
  76. package/dist/server/http.js.map +1 -0
  77. package/dist/server.d.ts +13 -0
  78. package/dist/server.d.ts.map +1 -0
  79. package/dist/server.js +157 -0
  80. package/dist/server.js.map +1 -0
  81. package/dist/session/manager.d.ts +91 -0
  82. package/dist/session/manager.d.ts.map +1 -0
  83. package/dist/session/manager.js +251 -0
  84. package/dist/session/manager.js.map +1 -0
  85. package/dist/tools/index.d.ts +11 -0
  86. package/dist/tools/index.d.ts.map +1 -0
  87. package/dist/tools/index.js +27 -0
  88. package/dist/tools/index.js.map +1 -0
  89. package/dist/tools/manage-subscription.d.ts +43 -0
  90. package/dist/tools/manage-subscription.d.ts.map +1 -0
  91. package/dist/tools/manage-subscription.js +268 -0
  92. package/dist/tools/manage-subscription.js.map +1 -0
  93. package/dist/tools/registry.d.ts +40 -0
  94. package/dist/tools/registry.d.ts.map +1 -0
  95. package/dist/tools/registry.js +85 -0
  96. package/dist/tools/registry.js.map +1 -0
  97. package/dist/tools/search-resources.d.ts +31 -0
  98. package/dist/tools/search-resources.d.ts.map +1 -0
  99. package/dist/tools/search-resources.js +154 -0
  100. package/dist/tools/search-resources.js.map +1 -0
  101. package/dist/tools/sync-resources.d.ts +41 -0
  102. package/dist/tools/sync-resources.d.ts.map +1 -0
  103. package/dist/tools/sync-resources.js +606 -0
  104. package/dist/tools/sync-resources.js.map +1 -0
  105. package/dist/tools/uninstall-resource.d.ts +30 -0
  106. package/dist/tools/uninstall-resource.d.ts.map +1 -0
  107. package/dist/tools/uninstall-resource.js +259 -0
  108. package/dist/tools/uninstall-resource.js.map +1 -0
  109. package/dist/tools/upload-resource.d.ts +77 -0
  110. package/dist/tools/upload-resource.d.ts.map +1 -0
  111. package/dist/tools/upload-resource.js +252 -0
  112. package/dist/tools/upload-resource.js.map +1 -0
  113. package/dist/transport/sse.d.ts +29 -0
  114. package/dist/transport/sse.d.ts.map +1 -0
  115. package/dist/transport/sse.js +271 -0
  116. package/dist/transport/sse.js.map +1 -0
  117. package/dist/types/errors.d.ts +60 -0
  118. package/dist/types/errors.d.ts.map +1 -0
  119. package/dist/types/errors.js +112 -0
  120. package/dist/types/errors.js.map +1 -0
  121. package/dist/types/index.d.ts +7 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +23 -0
  124. package/dist/types/index.js.map +1 -0
  125. package/dist/types/mcp.d.ts +50 -0
  126. package/dist/types/mcp.d.ts.map +1 -0
  127. package/dist/types/mcp.js +6 -0
  128. package/dist/types/mcp.js.map +1 -0
  129. package/dist/types/resources.d.ts +109 -0
  130. package/dist/types/resources.d.ts.map +1 -0
  131. package/dist/types/resources.js +7 -0
  132. package/dist/types/resources.js.map +1 -0
  133. package/dist/types/tools.d.ts +147 -0
  134. package/dist/types/tools.d.ts.map +1 -0
  135. package/dist/types/tools.js +6 -0
  136. package/dist/types/tools.js.map +1 -0
  137. package/dist/utils/cursor-paths.d.ts +49 -0
  138. package/dist/utils/cursor-paths.d.ts.map +1 -0
  139. package/dist/utils/cursor-paths.js +116 -0
  140. package/dist/utils/cursor-paths.js.map +1 -0
  141. package/dist/utils/log-cleaner.d.ts +18 -0
  142. package/dist/utils/log-cleaner.d.ts.map +1 -0
  143. package/dist/utils/log-cleaner.js +112 -0
  144. package/dist/utils/log-cleaner.js.map +1 -0
  145. package/dist/utils/logger.d.ts +59 -0
  146. package/dist/utils/logger.d.ts.map +1 -0
  147. package/dist/utils/logger.js +292 -0
  148. package/dist/utils/logger.js.map +1 -0
  149. package/dist/utils/validation.d.ts +58 -0
  150. package/dist/utils/validation.d.ts.map +1 -0
  151. package/dist/utils/validation.js +214 -0
  152. package/dist/utils/validation.js.map +1 -0
  153. package/package.json +58 -0
  154. package/src/api/cached-client.ts +144 -0
  155. package/src/api/client.ts +578 -0
  156. package/src/auth/index.ts +11 -0
  157. package/src/auth/middleware.ts +244 -0
  158. package/src/auth/permissions.ts +317 -0
  159. package/src/auth/token-validator.ts +294 -0
  160. package/src/cache/cache-manager.ts +243 -0
  161. package/src/cache/index.ts +6 -0
  162. package/src/cache/redis-client.ts +249 -0
  163. package/src/config/constants.ts +33 -0
  164. package/src/config/index.ts +228 -0
  165. package/src/filesystem/manager.ts +235 -0
  166. package/src/git/multi-source-manager.ts +333 -0
  167. package/src/git/operations.ts +93 -0
  168. package/src/index.ts +139 -0
  169. package/src/monitoring/health.ts +132 -0
  170. package/src/resources/index.ts +13 -0
  171. package/src/resources/loader.ts +530 -0
  172. package/src/server/http.ts +427 -0
  173. package/src/server.ts +191 -0
  174. package/src/session/manager.ts +296 -0
  175. package/src/tools/index.ts +11 -0
  176. package/src/tools/manage-subscription.ts +332 -0
  177. package/src/tools/registry.ts +97 -0
  178. package/src/tools/search-resources.ts +177 -0
  179. package/src/tools/sync-resources.ts +662 -0
  180. package/src/tools/uninstall-resource.ts +248 -0
  181. package/src/tools/upload-resource.ts +258 -0
  182. package/src/transport/sse.ts +308 -0
  183. package/src/types/errors.ts +146 -0
  184. package/src/types/index.ts +7 -0
  185. package/src/types/mcp.ts +61 -0
  186. package/src/types/resources.ts +141 -0
  187. package/src/types/tools.ts +175 -0
  188. package/src/utils/cursor-paths.ts +83 -0
  189. package/src/utils/log-cleaner.ts +92 -0
  190. package/src/utils/logger.ts +333 -0
  191. package/src/utils/validation.ts +262 -0
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Resource Loader Module
3
+ * Manages multi-source AI resource loading and indexing
4
+ *
5
+ * Implements AI Resources Multi-Source Architecture as defined in:
6
+ * @see Docs/AI-Resources-Multi-Source-Architecture.md
7
+ * @see AGENTS.md (AI Resources 开发约束)
8
+ */
9
+
10
+ import * as fs from 'fs/promises';
11
+ import * as path from 'path';
12
+ import { logger } from '../utils/logger';
13
+ import type {
14
+ AIResourcesConfig,
15
+ ResourceMetadata,
16
+ ResourceType,
17
+ ResourceConflict,
18
+ LoaderStats,
19
+ } from '../types/resources';
20
+
21
+ /**
22
+ * Resource Loader
23
+ *
24
+ * Key responsibilities:
25
+ * 1. Load and validate ai-resources-config.json
26
+ * 2. Scan resources from multiple sources
27
+ * 3. Build resource index with priority
28
+ * 4. Resolve resource name conflicts
29
+ * 5. Provide unified resource query interface
30
+ */
31
+ export class ResourceLoader {
32
+ private config: AIResourcesConfig | null = null;
33
+ private resourceIndex: Map<string, ResourceMetadata> = new Map();
34
+ private conflicts: ResourceConflict[] = [];
35
+ private stats: LoaderStats | null = null;
36
+ private loaded: boolean = false;
37
+
38
+ // Cache configuration
39
+ private cacheEnabled: boolean = false;
40
+ private cacheTTL: number = 300000; // 5 minutes default
41
+ private lastLoadTime: number = 0;
42
+
43
+ /**
44
+ * Load AI Resources configuration file
45
+ */
46
+ async loadConfig(configPath?: string): Promise<void> {
47
+ const startTime = Date.now();
48
+
49
+ try {
50
+ // Default config path
51
+ const defaultPath = path.resolve(process.cwd(), 'AI-Resources/ai-resources-config.json');
52
+ const finalPath = configPath || defaultPath;
53
+
54
+ logger.debug({ configPath: finalPath }, 'Loading AI Resources configuration...');
55
+
56
+ // Check if config file exists
57
+ try {
58
+ await fs.access(finalPath);
59
+ } catch {
60
+ logger.warn({ configPath: finalPath }, 'AI Resources config file not found, using default configuration');
61
+ this.config = this.getDefaultConfig();
62
+ return;
63
+ }
64
+
65
+ // Read and parse config file
66
+ const configContent = await fs.readFile(finalPath, 'utf-8');
67
+ const parsedConfig = JSON.parse(configContent) as AIResourcesConfig;
68
+
69
+ // Validate configuration
70
+ this.validateConfig(parsedConfig);
71
+
72
+ this.config = parsedConfig;
73
+ this.cacheEnabled = parsedConfig.cache?.enabled ?? true;
74
+ this.cacheTTL = (parsedConfig.cache?.ttl ?? 300) * 1000; // Convert to ms
75
+
76
+ const duration = Date.now() - startTime;
77
+ logger.info(
78
+ {
79
+ configPath: finalPath,
80
+ defaultSource: parsedConfig.default_source.name,
81
+ extendedSourcesCount: parsedConfig.extended_sources.length,
82
+ duration,
83
+ },
84
+ 'AI Resources configuration loaded successfully'
85
+ );
86
+ } catch (error) {
87
+ logger.error(
88
+ {
89
+ type: 'resource',
90
+ operation: 'load_config',
91
+ error: error instanceof Error ? error.message : String(error),
92
+ stack: error instanceof Error ? error.stack : undefined,
93
+ },
94
+ 'Failed to load AI Resources configuration'
95
+ );
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Validate configuration structure and constraints
102
+ */
103
+ private validateConfig(config: AIResourcesConfig): void {
104
+ // Check version
105
+ if (config.version !== '1.0') {
106
+ throw new Error(`Unsupported config version: ${config.version}, expected 1.0`);
107
+ }
108
+
109
+ // Check default source
110
+ if (!config.default_source) {
111
+ throw new Error('Missing required field: default_source');
112
+ }
113
+
114
+ if (config.default_source.name !== 'csp') {
115
+ logger.warn(
116
+ { name: config.default_source.name },
117
+ 'Default source name is not "csp", this may cause compatibility issues'
118
+ );
119
+ }
120
+
121
+ if (config.default_source.priority !== 100) {
122
+ logger.warn(
123
+ { priority: config.default_source.priority },
124
+ 'Default source priority is not 100, this may affect conflict resolution'
125
+ );
126
+ }
127
+
128
+ if (!config.default_source.enabled) {
129
+ throw new Error('Default source must be enabled');
130
+ }
131
+
132
+ // Check resource types
133
+ const expectedTypes: ResourceType[] = ['commands', 'skills', 'mcp', 'rules'];
134
+ if (
135
+ !config.resource_types ||
136
+ config.resource_types.length !== 4 ||
137
+ !expectedTypes.every((type) => config.resource_types.includes(type))
138
+ ) {
139
+ throw new Error(
140
+ `Invalid resource_types, expected: ${expectedTypes.join(', ')}, got: ${config.resource_types?.join(', ') || 'none'}`
141
+ );
142
+ }
143
+
144
+ // Check loading order
145
+ if (config.loading_order !== 'priority_desc') {
146
+ logger.warn(
147
+ { loading_order: config.loading_order },
148
+ 'Loading order is not "priority_desc", this may affect resource precedence'
149
+ );
150
+ }
151
+
152
+ // Check conflict resolution
153
+ if (config.conflict_resolution !== 'highest_priority_wins') {
154
+ logger.warn(
155
+ { conflict_resolution: config.conflict_resolution },
156
+ 'Conflict resolution is not "highest_priority_wins", this may affect behavior'
157
+ );
158
+ }
159
+
160
+ // Check extended sources priority
161
+ const defaultPriority = config.default_source.priority;
162
+ for (const source of config.extended_sources || []) {
163
+ if (source.priority >= defaultPriority) {
164
+ throw new Error(
165
+ `Extended source "${source.name}" has priority ${source.priority} which is >= default source priority ${defaultPriority}. Default source must have highest priority.`
166
+ );
167
+ }
168
+ }
169
+
170
+ logger.debug('AI Resources configuration validation passed');
171
+ }
172
+
173
+ /**
174
+ * Get default configuration (fallback)
175
+ */
176
+ private getDefaultConfig(): AIResourcesConfig {
177
+ return {
178
+ version: '1.0',
179
+ description: 'Default AI Resources configuration',
180
+ default_source: {
181
+ name: 'csp',
182
+ path: 'csp/ai-resources',
183
+ enabled: true,
184
+ priority: 100,
185
+ resources: {
186
+ commands: 'commands',
187
+ skills: 'skills',
188
+ mcp: 'mcp',
189
+ rules: 'rules',
190
+ },
191
+ },
192
+ extended_sources: [],
193
+ resource_types: ['commands', 'skills', 'mcp', 'rules'],
194
+ loading_order: 'priority_desc',
195
+ conflict_resolution: 'highest_priority_wins',
196
+ cache: {
197
+ enabled: true,
198
+ ttl: 300,
199
+ },
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Scan and index resources from all sources
205
+ */
206
+ async scanResources(): Promise<void> {
207
+ if (!this.config) {
208
+ throw new Error('Configuration not loaded. Call loadConfig() first.');
209
+ }
210
+
211
+ // Check cache validity
212
+ if (this.cacheEnabled && this.loaded) {
213
+ const cacheAge = Date.now() - this.lastLoadTime;
214
+ if (cacheAge < this.cacheTTL) {
215
+ logger.debug({ cacheAge }, 'Using cached resource index');
216
+ return;
217
+ }
218
+ logger.debug('Resource cache expired, reloading...');
219
+ }
220
+
221
+ const startTime = Date.now();
222
+ this.resourceIndex.clear();
223
+ this.conflicts = [];
224
+
225
+ const stats = {
226
+ sourcesLoaded: 0,
227
+ resourcesIndexed: 0,
228
+ byType: {
229
+ commands: 0,
230
+ skills: 0,
231
+ mcp: 0,
232
+ rules: 0,
233
+ } as Record<ResourceType, number>,
234
+ conflictsDetected: 0,
235
+ loadDuration: 0,
236
+ };
237
+
238
+ try {
239
+ // Collect all sources sorted by priority (descending)
240
+ const sources = [this.config.default_source, ...this.config.extended_sources.filter((s) => s.enabled)];
241
+ sources.sort((a, b) => b.priority - a.priority); // High priority first
242
+
243
+ logger.info({ sourceCount: sources.length }, 'Scanning resources from all sources...');
244
+
245
+ // Scan each source
246
+ for (const source of sources) {
247
+ await this.scanSource(source, stats);
248
+ stats.sourcesLoaded++;
249
+ }
250
+
251
+ stats.conflictsDetected = this.conflicts.length;
252
+ stats.loadDuration = Date.now() - startTime;
253
+ this.stats = stats;
254
+ this.loaded = true;
255
+ this.lastLoadTime = Date.now();
256
+
257
+ logger.info(
258
+ {
259
+ sourcesLoaded: stats.sourcesLoaded,
260
+ resourcesIndexed: stats.resourcesIndexed,
261
+ conflictsDetected: stats.conflictsDetected,
262
+ duration: stats.loadDuration,
263
+ },
264
+ 'Resource scanning completed'
265
+ );
266
+ } catch (error) {
267
+ logger.error(
268
+ {
269
+ type: 'resource',
270
+ operation: 'scan_resources',
271
+ error: error instanceof Error ? error.message : String(error),
272
+ },
273
+ 'Failed to scan resources'
274
+ );
275
+ throw error;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Scan resources from a single source
281
+ */
282
+ private async scanSource(
283
+ source: AIResourcesConfig['default_source'],
284
+ stats: LoaderStats
285
+ ): Promise<void> {
286
+ logger.debug({ source: source.name, path: source.path }, 'Scanning source...');
287
+
288
+ const baseDir = path.resolve(process.cwd(), 'AI-Resources', source.path);
289
+
290
+ // Check if source directory exists
291
+ try {
292
+ await fs.access(baseDir);
293
+ } catch {
294
+ logger.warn({ source: source.name, path: baseDir }, 'Source directory not found, skipping');
295
+ return;
296
+ }
297
+
298
+ // Scan each resource type
299
+ for (const type of this.config!.resource_types) {
300
+ const subDir = source.resources[type];
301
+ if (!subDir) {
302
+ logger.debug({ source: source.name, type }, 'Resource type not defined for this source, skipping');
303
+ continue;
304
+ }
305
+
306
+ const resourceDir = path.join(baseDir, subDir);
307
+
308
+ try {
309
+ await fs.access(resourceDir);
310
+ } catch {
311
+ logger.debug({ source: source.name, type, path: resourceDir }, 'Resource directory not found, skipping');
312
+ continue;
313
+ }
314
+
315
+ await this.scanResourceType(source, type, resourceDir, stats);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Scan resources of a specific type from a directory
321
+ */
322
+ private async scanResourceType(
323
+ source: AIResourcesConfig['default_source'],
324
+ type: ResourceType,
325
+ dir: string,
326
+ stats: LoaderStats
327
+ ): Promise<void> {
328
+ try {
329
+ const entries = await fs.readdir(dir, { withFileTypes: true });
330
+
331
+ for (const entry of entries) {
332
+ const fullPath = path.join(dir, entry.name);
333
+
334
+ if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdc'))) {
335
+ // Handle file resources (commands, rules)
336
+ const resourceName = path.basename(entry.name, path.extname(entry.name));
337
+ await this.indexResource(source, type, resourceName, fullPath, stats);
338
+ } else if (entry.isDirectory()) {
339
+ // Handle directory resources (skills, mcp)
340
+ const skillFile = path.join(fullPath, 'SKILL.md');
341
+ try {
342
+ await fs.access(skillFile);
343
+ await this.indexResource(source, type, entry.name, skillFile, stats);
344
+ } catch {
345
+ // Not a skill directory, skip
346
+ logger.debug({ dir: fullPath }, 'Directory does not contain SKILL.md, skipping');
347
+ }
348
+ }
349
+ }
350
+ } catch (error) {
351
+ logger.warn(
352
+ {
353
+ source: source.name,
354
+ type,
355
+ dir,
356
+ error: error instanceof Error ? error.message : String(error),
357
+ },
358
+ 'Failed to scan resource directory'
359
+ );
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Index a single resource with conflict detection
365
+ */
366
+ private async indexResource(
367
+ source: AIResourcesConfig['default_source'],
368
+ type: ResourceType,
369
+ name: string,
370
+ filePath: string,
371
+ stats: LoaderStats
372
+ ): Promise<void> {
373
+ const resourceKey = `${type}:${name}`;
374
+ const existing = this.resourceIndex.get(resourceKey);
375
+
376
+ if (existing) {
377
+ // Conflict detected
378
+ logger.warn(
379
+ {
380
+ resourceName: name,
381
+ type,
382
+ existingSource: existing.source,
383
+ existingPriority: existing.priority,
384
+ newSource: source.name,
385
+ newPriority: source.priority,
386
+ },
387
+ 'Resource name conflict detected'
388
+ );
389
+
390
+ // Record conflict
391
+ const conflict: ResourceConflict = {
392
+ name,
393
+ type,
394
+ conflicts: [
395
+ {
396
+ source: existing.source,
397
+ priority: existing.priority,
398
+ path: existing.path,
399
+ },
400
+ {
401
+ source: source.name,
402
+ priority: source.priority,
403
+ path: filePath,
404
+ },
405
+ ],
406
+ selected:
407
+ source.priority > existing.priority
408
+ ? { source: source.name, priority: source.priority, path: filePath }
409
+ : { source: existing.source, priority: existing.priority, path: existing.path },
410
+ };
411
+
412
+ this.conflicts.push(conflict);
413
+
414
+ // Keep higher priority resource (already sorted by priority)
415
+ if (source.priority <= existing.priority) {
416
+ logger.debug(
417
+ {
418
+ resourceName: name,
419
+ selectedSource: existing.source,
420
+ },
421
+ 'Keeping existing resource (higher priority)'
422
+ );
423
+ return;
424
+ }
425
+
426
+ logger.debug(
427
+ {
428
+ resourceName: name,
429
+ selectedSource: source.name,
430
+ },
431
+ 'Replacing with new resource (higher priority)'
432
+ );
433
+ }
434
+
435
+ // Index resource
436
+ const metadata: ResourceMetadata = {
437
+ id: resourceKey,
438
+ name,
439
+ type,
440
+ source: source.name,
441
+ priority: source.priority,
442
+ path: filePath,
443
+ };
444
+
445
+ this.resourceIndex.set(resourceKey, metadata);
446
+ stats.resourcesIndexed++;
447
+ stats.byType[type]++;
448
+
449
+ logger.debug({ resourceKey, source: source.name }, 'Resource indexed');
450
+ }
451
+
452
+ /**
453
+ * Get all resources by type
454
+ */
455
+ getResourcesByType(type: ResourceType): ResourceMetadata[] {
456
+ this.ensureLoaded();
457
+ const results: ResourceMetadata[] = [];
458
+ for (const [, metadata] of this.resourceIndex) {
459
+ if (metadata.type === type) {
460
+ results.push(metadata);
461
+ }
462
+ }
463
+ return results;
464
+ }
465
+
466
+ /**
467
+ * Get resource by ID (type:name)
468
+ */
469
+ getResourceById(id: string): ResourceMetadata | null {
470
+ this.ensureLoaded();
471
+ return this.resourceIndex.get(id) || null;
472
+ }
473
+
474
+ /**
475
+ * Search resources by name
476
+ */
477
+ searchResourcesByName(name: string, type?: ResourceType): ResourceMetadata[] {
478
+ this.ensureLoaded();
479
+ const results: ResourceMetadata[] = [];
480
+ const lowerName = name.toLowerCase();
481
+
482
+ for (const metadata of this.resourceIndex.values()) {
483
+ if (type && metadata.type !== type) {
484
+ continue;
485
+ }
486
+ if (metadata.name.toLowerCase().includes(lowerName)) {
487
+ results.push(metadata);
488
+ }
489
+ }
490
+
491
+ return results;
492
+ }
493
+
494
+ /**
495
+ * Get all detected conflicts
496
+ */
497
+ getConflicts(): ResourceConflict[] {
498
+ this.ensureLoaded();
499
+ return [...this.conflicts];
500
+ }
501
+
502
+ /**
503
+ * Get loader statistics
504
+ */
505
+ getStats(): LoaderStats | null {
506
+ return this.stats;
507
+ }
508
+
509
+ /**
510
+ * Refresh resource index (clear cache and rescan)
511
+ */
512
+ async refresh(): Promise<void> {
513
+ logger.info('Refreshing resource index...');
514
+ this.loaded = false;
515
+ this.lastLoadTime = 0;
516
+ await this.scanResources();
517
+ }
518
+
519
+ /**
520
+ * Ensure resources are loaded
521
+ */
522
+ private ensureLoaded(): void {
523
+ if (!this.loaded) {
524
+ throw new Error('Resources not loaded. Call scanResources() first.');
525
+ }
526
+ }
527
+ }
528
+
529
+ // Singleton instance
530
+ export const resourceLoader = new ResourceLoader();