@adversity/coding-tool-x 3.0.4 → 3.0.6

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.
@@ -0,0 +1,762 @@
1
+ /**
2
+ * Config Registry Service
3
+ *
4
+ * Manages a unified config registry at ~/.claude/cc-tool/config-registry.json
5
+ * that tracks skills, commands, agents, rules with enable/disable and per-platform support.
6
+ *
7
+ * Storage directories: ~/.claude/cc-tool/configs/{skills,commands,agents,rules}/
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ // Configuration paths
15
+ const CC_TOOL_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
16
+ const REGISTRY_FILE = path.join(CC_TOOL_DIR, 'config-registry.json');
17
+ const CONFIGS_DIR = path.join(CC_TOOL_DIR, 'configs');
18
+
19
+ // Claude Code native directories
20
+ const CLAUDE_DIRS = {
21
+ skills: path.join(os.homedir(), '.claude', 'skills'),
22
+ commands: path.join(os.homedir(), '.claude', 'commands'),
23
+ agents: path.join(os.homedir(), '.claude', 'agents'),
24
+ rules: path.join(os.homedir(), '.claude', 'rules'),
25
+ plugins: path.join(os.homedir(), '.claude', 'plugins')
26
+ };
27
+
28
+ // Valid config types
29
+ const CONFIG_TYPES = ['skills', 'commands', 'agents', 'rules', 'plugins'];
30
+
31
+ // Default registry structure
32
+ const DEFAULT_REGISTRY = {
33
+ version: 1,
34
+ skills: {},
35
+ commands: {},
36
+ agents: {},
37
+ rules: {},
38
+ plugins: {}
39
+ };
40
+
41
+ /**
42
+ * Ensure directory exists
43
+ */
44
+ function ensureDir(dirPath) {
45
+ if (!fs.existsSync(dirPath)) {
46
+ fs.mkdirSync(dirPath, { recursive: true });
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Config Registry Service
52
+ */
53
+ class ConfigRegistryService {
54
+ constructor() {
55
+ this.registryPath = REGISTRY_FILE;
56
+ this.configsDir = CONFIGS_DIR;
57
+
58
+ // Ensure directories exist
59
+ this._ensureDirs();
60
+ }
61
+
62
+ /**
63
+ * Ensure all required directories exist
64
+ * @private
65
+ */
66
+ _ensureDirs() {
67
+ ensureDir(CC_TOOL_DIR);
68
+ ensureDir(this.configsDir);
69
+
70
+ for (const type of CONFIG_TYPES) {
71
+ ensureDir(path.join(this.configsDir, type));
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Read registry from file
77
+ * @returns {Object} Registry data
78
+ */
79
+ _readRegistry() {
80
+ try {
81
+ if (fs.existsSync(this.registryPath)) {
82
+ const content = fs.readFileSync(this.registryPath, 'utf-8');
83
+ const data = JSON.parse(content);
84
+
85
+ // Ensure all type keys exist
86
+ for (const type of CONFIG_TYPES) {
87
+ if (!data[type]) {
88
+ data[type] = {};
89
+ }
90
+ }
91
+
92
+ return data;
93
+ }
94
+ } catch (err) {
95
+ console.error('[ConfigRegistry] Failed to read registry:', err.message);
96
+ }
97
+
98
+ return { ...DEFAULT_REGISTRY };
99
+ }
100
+
101
+ /**
102
+ * Write registry to file (atomic write via temp file + rename)
103
+ * @param {Object} data - Registry data to write
104
+ */
105
+ _writeRegistry(data) {
106
+ try {
107
+ ensureDir(path.dirname(this.registryPath));
108
+
109
+ const tempPath = this.registryPath + '.tmp';
110
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
111
+ fs.renameSync(tempPath, this.registryPath);
112
+ } catch (err) {
113
+ console.error('[ConfigRegistry] Failed to write registry:', err.message);
114
+ throw err;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Get a single item from registry
120
+ * @param {string} type - Config type (skills, commands, agents, rules)
121
+ * @param {string} name - Item name/key
122
+ * @returns {Object|null} Registry entry or null
123
+ */
124
+ getItem(type, name) {
125
+ if (!CONFIG_TYPES.includes(type)) {
126
+ throw new Error(`Invalid config type: ${type}`);
127
+ }
128
+
129
+ const registry = this._readRegistry();
130
+ return registry[type][name] || null;
131
+ }
132
+
133
+ /**
134
+ * Set/update an item in registry
135
+ * @param {string} type - Config type
136
+ * @param {string} name - Item name/key
137
+ * @param {Object} data - Item data
138
+ * @returns {Object} Updated entry
139
+ */
140
+ setItem(type, name, data) {
141
+ if (!CONFIG_TYPES.includes(type)) {
142
+ throw new Error(`Invalid config type: ${type}`);
143
+ }
144
+
145
+ const registry = this._readRegistry();
146
+ const now = new Date().toISOString();
147
+
148
+ const existing = registry[type][name];
149
+ const entry = {
150
+ enabled: data.enabled !== undefined ? data.enabled : true,
151
+ platforms: data.platforms || { claude: true, codex: false },
152
+ createdAt: existing?.createdAt || now,
153
+ updatedAt: now,
154
+ source: data.source || 'local',
155
+ ...data
156
+ };
157
+
158
+ registry[type][name] = entry;
159
+ this._writeRegistry(registry);
160
+
161
+ return entry;
162
+ }
163
+
164
+ /**
165
+ * Remove an item from registry
166
+ * @param {string} type - Config type
167
+ * @param {string} name - Item name/key
168
+ * @returns {boolean} True if removed, false if not found
169
+ */
170
+ removeItem(type, name) {
171
+ if (!CONFIG_TYPES.includes(type)) {
172
+ throw new Error(`Invalid config type: ${type}`);
173
+ }
174
+
175
+ const registry = this._readRegistry();
176
+
177
+ if (!registry[type][name]) {
178
+ return false;
179
+ }
180
+
181
+ delete registry[type][name];
182
+ this._writeRegistry(registry);
183
+
184
+ // Also remove the actual config files
185
+ const configPath = this.getConfigPath(type, name);
186
+ if (fs.existsSync(configPath)) {
187
+ try {
188
+ const stats = fs.statSync(configPath);
189
+ if (stats.isDirectory()) {
190
+ fs.rmSync(configPath, { recursive: true, force: true });
191
+ } else {
192
+ fs.unlinkSync(configPath);
193
+ }
194
+ } catch (err) {
195
+ console.error(`[ConfigRegistry] Failed to remove config files for ${type}/${name}:`, err.message);
196
+ }
197
+ }
198
+
199
+ return true;
200
+ }
201
+
202
+ /**
203
+ * List all items of a type
204
+ * @param {string} type - Config type
205
+ * @returns {Object} { name: registryEntry } map
206
+ */
207
+ listItems(type) {
208
+ if (!CONFIG_TYPES.includes(type)) {
209
+ throw new Error(`Invalid config type: ${type}`);
210
+ }
211
+
212
+ const registry = this._readRegistry();
213
+ return registry[type] || {};
214
+ }
215
+
216
+ /**
217
+ * Toggle enabled status for an item
218
+ * @param {string} type - Config type
219
+ * @param {string} name - Item name/key
220
+ * @param {boolean} enabled - New enabled status
221
+ * @returns {Object} Updated entry
222
+ */
223
+ toggleEnabled(type, name, enabled) {
224
+ if (!CONFIG_TYPES.includes(type)) {
225
+ throw new Error(`Invalid config type: ${type}`);
226
+ }
227
+
228
+ const registry = this._readRegistry();
229
+ const entry = registry[type][name];
230
+
231
+ if (!entry) {
232
+ throw new Error(`Item "${name}" not found in ${type}`);
233
+ }
234
+
235
+ entry.enabled = enabled;
236
+ entry.updatedAt = new Date().toISOString();
237
+
238
+ this._writeRegistry(registry);
239
+
240
+ return entry;
241
+ }
242
+
243
+ /**
244
+ * Toggle platform support for an item
245
+ * @param {string} type - Config type
246
+ * @param {string} name - Item name/key
247
+ * @param {string} platform - Platform name (claude, codex)
248
+ * @param {boolean} enabled - New platform status
249
+ * @returns {Object} Updated entry
250
+ */
251
+ togglePlatform(type, name, platform, enabled) {
252
+ if (!CONFIG_TYPES.includes(type)) {
253
+ throw new Error(`Invalid config type: ${type}`);
254
+ }
255
+
256
+ if (!['claude', 'codex'].includes(platform)) {
257
+ throw new Error(`Invalid platform: ${platform}`);
258
+ }
259
+
260
+ const registry = this._readRegistry();
261
+ const entry = registry[type][name];
262
+
263
+ if (!entry) {
264
+ throw new Error(`Item "${name}" not found in ${type}`);
265
+ }
266
+
267
+ if (!entry.platforms) {
268
+ entry.platforms = { claude: true, codex: false };
269
+ }
270
+
271
+ entry.platforms[platform] = enabled;
272
+ entry.updatedAt = new Date().toISOString();
273
+
274
+ this._writeRegistry(registry);
275
+
276
+ return entry;
277
+ }
278
+
279
+ /**
280
+ * Import configs from Claude Code native directories
281
+ * @param {string} type - Config type to import
282
+ * @returns {Object} { imported: number, skipped: number, items: string[] }
283
+ */
284
+ importFromClaude(type) {
285
+ if (!CONFIG_TYPES.includes(type)) {
286
+ throw new Error(`Invalid config type: ${type}`);
287
+ }
288
+
289
+ const sourceDir = CLAUDE_DIRS[type];
290
+ const destDir = path.join(this.configsDir, type);
291
+
292
+ if (!fs.existsSync(sourceDir)) {
293
+ return { imported: 0, skipped: 0, items: [] };
294
+ }
295
+
296
+ const registry = this._readRegistry();
297
+ const result = {
298
+ imported: 0,
299
+ skipped: 0,
300
+ items: []
301
+ };
302
+
303
+ if (type === 'skills') {
304
+ // Skills are directory-based with SKILL.md marker
305
+ this._importSkills(sourceDir, destDir, registry, result);
306
+ } else if (type === 'plugins') {
307
+ // Plugins are directory-based (similar to skills)
308
+ this._importPlugins(sourceDir, destDir, registry, result);
309
+ } else {
310
+ // Commands, agents, rules are file-based (.md files)
311
+ this._importFileBasedConfigs(type, sourceDir, destDir, '', registry, result);
312
+ }
313
+
314
+ this._writeRegistry(registry);
315
+
316
+ return result;
317
+ }
318
+
319
+ /**
320
+ * Import skills (directory-based)
321
+ * @private
322
+ */
323
+ _importSkills(sourceDir, destDir, registry, result) {
324
+ try {
325
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
326
+
327
+ for (const entry of entries) {
328
+ if (!entry.isDirectory() || entry.name.startsWith('.')) {
329
+ continue;
330
+ }
331
+
332
+ const skillDir = path.join(sourceDir, entry.name);
333
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
334
+
335
+ // Check if it's a valid skill directory
336
+ if (!fs.existsSync(skillMdPath)) {
337
+ continue;
338
+ }
339
+
340
+ const name = entry.name;
341
+
342
+ // Skip if already in registry
343
+ if (registry.skills[name]) {
344
+ result.skipped++;
345
+ continue;
346
+ }
347
+
348
+ // Copy to cc-tool configs
349
+ const destPath = path.join(destDir, name);
350
+ try {
351
+ this._copyDirRecursive(skillDir, destPath);
352
+
353
+ // Add to registry
354
+ registry.skills[name] = {
355
+ enabled: true,
356
+ platforms: { claude: true, codex: false },
357
+ createdAt: new Date().toISOString(),
358
+ updatedAt: new Date().toISOString(),
359
+ source: 'imported'
360
+ };
361
+
362
+ result.imported++;
363
+ result.items.push(name);
364
+ } catch (err) {
365
+ console.error(`[ConfigRegistry] Failed to import skill "${name}":`, err.message);
366
+ }
367
+ }
368
+ } catch (err) {
369
+ console.error('[ConfigRegistry] Failed to scan skills directory:', err.message);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Import plugins (directory-based, similar to skills)
375
+ * Plugins are directories containing plugin.json or similar marker
376
+ * @private
377
+ */
378
+ _importPlugins(sourceDir, destDir, registry, result) {
379
+ try {
380
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
381
+
382
+ for (const entry of entries) {
383
+ if (!entry.isDirectory() || entry.name.startsWith('.')) {
384
+ continue;
385
+ }
386
+
387
+ const pluginDir = path.join(sourceDir, entry.name);
388
+
389
+ // Check if it's a valid plugin directory (has plugin.json or any content)
390
+ // Plugins may have various structures, so we just check it's a non-empty directory
391
+ const contents = fs.readdirSync(pluginDir);
392
+ if (contents.length === 0) {
393
+ continue;
394
+ }
395
+
396
+ const name = entry.name;
397
+
398
+ // Skip if already in registry
399
+ if (registry.plugins[name]) {
400
+ result.skipped++;
401
+ continue;
402
+ }
403
+
404
+ // Copy to cc-tool configs
405
+ const destPath = path.join(destDir, name);
406
+ try {
407
+ this._copyDirRecursive(pluginDir, destPath);
408
+
409
+ // Add to registry
410
+ registry.plugins[name] = {
411
+ enabled: true,
412
+ platforms: { claude: true, codex: false },
413
+ createdAt: new Date().toISOString(),
414
+ updatedAt: new Date().toISOString(),
415
+ source: 'imported'
416
+ };
417
+
418
+ result.imported++;
419
+ result.items.push(name);
420
+ } catch (err) {
421
+ console.error(`[ConfigRegistry] Failed to import plugin "${name}":`, err.message);
422
+ }
423
+ }
424
+ } catch (err) {
425
+ console.error('[ConfigRegistry] Failed to scan plugins directory:', err.message);
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Import file-based configs (commands, agents, rules)
431
+ * @private
432
+ */
433
+ _importFileBasedConfigs(type, sourceDir, destDir, relativePath, registry, result) {
434
+ try {
435
+ const currentSource = relativePath ? path.join(sourceDir, relativePath) : sourceDir;
436
+
437
+ if (!fs.existsSync(currentSource)) {
438
+ return;
439
+ }
440
+
441
+ const entries = fs.readdirSync(currentSource, { withFileTypes: true });
442
+
443
+ for (const entry of entries) {
444
+ if (entry.name.startsWith('.')) {
445
+ continue;
446
+ }
447
+
448
+ const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
449
+
450
+ if (entry.isDirectory()) {
451
+ // Recursively scan subdirectories
452
+ this._importFileBasedConfigs(type, sourceDir, destDir, entryRelativePath, registry, result);
453
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
454
+ // This is a config file
455
+ const name = entryRelativePath; // Key is the relative path
456
+
457
+ // Skip if already in registry
458
+ if (registry[type][name]) {
459
+ result.skipped++;
460
+ continue;
461
+ }
462
+
463
+ // Copy file to cc-tool configs
464
+ const sourcePath = path.join(sourceDir, entryRelativePath);
465
+ const destPath = path.join(destDir, entryRelativePath);
466
+
467
+ try {
468
+ // Ensure destination directory exists
469
+ ensureDir(path.dirname(destPath));
470
+
471
+ fs.copyFileSync(sourcePath, destPath);
472
+
473
+ // Add to registry
474
+ registry[type][name] = {
475
+ enabled: true,
476
+ platforms: { claude: true, codex: false },
477
+ createdAt: new Date().toISOString(),
478
+ updatedAt: new Date().toISOString(),
479
+ source: 'imported'
480
+ };
481
+
482
+ result.imported++;
483
+ result.items.push(name);
484
+ } catch (err) {
485
+ console.error(`[ConfigRegistry] Failed to import ${type}/${name}:`, err.message);
486
+ }
487
+ }
488
+ }
489
+ } catch (err) {
490
+ console.error(`[ConfigRegistry] Failed to scan ${type} directory:`, err.message);
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Recursively copy a directory
496
+ * @private
497
+ */
498
+ _copyDirRecursive(src, dest) {
499
+ ensureDir(dest);
500
+
501
+ const entries = fs.readdirSync(src, { withFileTypes: true });
502
+
503
+ for (const entry of entries) {
504
+ const srcPath = path.join(src, entry.name);
505
+ const destPath = path.join(dest, entry.name);
506
+
507
+ if (entry.isDirectory()) {
508
+ this._copyDirRecursive(srcPath, destPath);
509
+ } else {
510
+ fs.copyFileSync(srcPath, destPath);
511
+ }
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Get statistics for all config types
517
+ * @returns {Object} Stats with counts per type and enabled/disabled breakdown
518
+ */
519
+ getStats() {
520
+ const registry = this._readRegistry();
521
+ const stats = {
522
+ total: 0,
523
+ byType: {},
524
+ byPlatform: {
525
+ claude: 0,
526
+ codex: 0
527
+ }
528
+ };
529
+
530
+ for (const type of CONFIG_TYPES) {
531
+ const items = Object.values(registry[type] || {});
532
+ const typeStats = {
533
+ total: items.length,
534
+ enabled: items.filter(i => i.enabled).length,
535
+ disabled: items.filter(i => !i.enabled).length,
536
+ claude: items.filter(i => i.platforms?.claude).length,
537
+ codex: items.filter(i => i.platforms?.codex).length
538
+ };
539
+
540
+ stats.byType[type] = typeStats;
541
+ stats.total += typeStats.total;
542
+ stats.byPlatform.claude += typeStats.claude;
543
+ stats.byPlatform.codex += typeStats.codex;
544
+ }
545
+
546
+ return stats;
547
+ }
548
+
549
+ /**
550
+ * Get the path to a config in cc-tool storage
551
+ * @param {string} type - Config type
552
+ * @param {string} name - Item name/key
553
+ * @returns {string} Full path to config
554
+ */
555
+ getConfigPath(type, name) {
556
+ if (!CONFIG_TYPES.includes(type)) {
557
+ throw new Error(`Invalid config type: ${type}`);
558
+ }
559
+
560
+ return path.join(this.configsDir, type, name);
561
+ }
562
+
563
+ /**
564
+ * Check if a config file/directory exists in storage
565
+ * @param {string} type - Config type
566
+ * @param {string} name - Item name/key
567
+ * @returns {boolean} True if exists
568
+ */
569
+ configExists(type, name) {
570
+ const configPath = this.getConfigPath(type, name);
571
+ return fs.existsSync(configPath);
572
+ }
573
+
574
+ /**
575
+ * Get config content
576
+ * @param {string} type - Config type
577
+ * @param {string} name - Item name/key
578
+ * @returns {string|null} File content or null
579
+ */
580
+ getConfigContent(type, name) {
581
+ const configPath = this.getConfigPath(type, name);
582
+
583
+ if (!fs.existsSync(configPath)) {
584
+ return null;
585
+ }
586
+
587
+ const stats = fs.statSync(configPath);
588
+
589
+ if (stats.isDirectory()) {
590
+ // For skills, return SKILL.md content
591
+ const skillMdPath = path.join(configPath, 'SKILL.md');
592
+ if (fs.existsSync(skillMdPath)) {
593
+ return fs.readFileSync(skillMdPath, 'utf-8');
594
+ }
595
+ return null;
596
+ }
597
+
598
+ return fs.readFileSync(configPath, 'utf-8');
599
+ }
600
+
601
+ /**
602
+ * Sync registry with actual files in storage
603
+ * Adds missing entries, removes orphaned entries
604
+ * @returns {Object} { added: number, removed: number }
605
+ */
606
+ syncRegistry() {
607
+ const registry = this._readRegistry();
608
+ const result = { added: 0, removed: 0 };
609
+
610
+ for (const type of CONFIG_TYPES) {
611
+ const typeDir = path.join(this.configsDir, type);
612
+
613
+ if (!fs.existsSync(typeDir)) {
614
+ continue;
615
+ }
616
+
617
+ // Find orphaned registry entries (file deleted)
618
+ for (const name of Object.keys(registry[type])) {
619
+ if (!this.configExists(type, name)) {
620
+ delete registry[type][name];
621
+ result.removed++;
622
+ }
623
+ }
624
+
625
+ // Find missing registry entries (file exists but not registered)
626
+ if (type === 'skills') {
627
+ this._syncSkillsRegistry(typeDir, registry, result);
628
+ } else if (type === 'plugins') {
629
+ this._syncPluginsRegistry(typeDir, registry, result);
630
+ } else {
631
+ this._syncFileBasedRegistry(type, typeDir, '', registry, result);
632
+ }
633
+ }
634
+
635
+ this._writeRegistry(registry);
636
+
637
+ return result;
638
+ }
639
+
640
+ /**
641
+ * Sync skills registry
642
+ * @private
643
+ */
644
+ _syncSkillsRegistry(typeDir, registry, result) {
645
+ try {
646
+ const entries = fs.readdirSync(typeDir, { withFileTypes: true });
647
+
648
+ for (const entry of entries) {
649
+ if (!entry.isDirectory() || entry.name.startsWith('.')) {
650
+ continue;
651
+ }
652
+
653
+ const skillMdPath = path.join(typeDir, entry.name, 'SKILL.md');
654
+ if (!fs.existsSync(skillMdPath)) {
655
+ continue;
656
+ }
657
+
658
+ const name = entry.name;
659
+
660
+ if (!registry.skills[name]) {
661
+ registry.skills[name] = {
662
+ enabled: true,
663
+ platforms: { claude: true, codex: false },
664
+ createdAt: new Date().toISOString(),
665
+ updatedAt: new Date().toISOString(),
666
+ source: 'synced'
667
+ };
668
+ result.added++;
669
+ }
670
+ }
671
+ } catch (err) {
672
+ console.error('[ConfigRegistry] Failed to sync skills registry:', err.message);
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Sync plugins registry
678
+ * @private
679
+ */
680
+ _syncPluginsRegistry(typeDir, registry, result) {
681
+ try {
682
+ const entries = fs.readdirSync(typeDir, { withFileTypes: true });
683
+
684
+ for (const entry of entries) {
685
+ if (!entry.isDirectory() || entry.name.startsWith('.')) {
686
+ continue;
687
+ }
688
+
689
+ const pluginDir = path.join(typeDir, entry.name);
690
+ const contents = fs.readdirSync(pluginDir);
691
+ if (contents.length === 0) {
692
+ continue;
693
+ }
694
+
695
+ const name = entry.name;
696
+
697
+ if (!registry.plugins[name]) {
698
+ registry.plugins[name] = {
699
+ enabled: true,
700
+ platforms: { claude: true, codex: false },
701
+ createdAt: new Date().toISOString(),
702
+ updatedAt: new Date().toISOString(),
703
+ source: 'synced'
704
+ };
705
+ result.added++;
706
+ }
707
+ }
708
+ } catch (err) {
709
+ console.error('[ConfigRegistry] Failed to sync plugins registry:', err.message);
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Sync file-based config registry
715
+ * @private
716
+ */
717
+ _syncFileBasedRegistry(type, baseDir, relativePath, registry, result) {
718
+ try {
719
+ const currentDir = relativePath ? path.join(baseDir, relativePath) : baseDir;
720
+
721
+ if (!fs.existsSync(currentDir)) {
722
+ return;
723
+ }
724
+
725
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
726
+
727
+ for (const entry of entries) {
728
+ if (entry.name.startsWith('.')) {
729
+ continue;
730
+ }
731
+
732
+ const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
733
+
734
+ if (entry.isDirectory()) {
735
+ this._syncFileBasedRegistry(type, baseDir, entryRelativePath, registry, result);
736
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
737
+ const name = entryRelativePath;
738
+
739
+ if (!registry[type][name]) {
740
+ registry[type][name] = {
741
+ enabled: true,
742
+ platforms: { claude: true, codex: false },
743
+ createdAt: new Date().toISOString(),
744
+ updatedAt: new Date().toISOString(),
745
+ source: 'synced'
746
+ };
747
+ result.added++;
748
+ }
749
+ }
750
+ }
751
+ } catch (err) {
752
+ console.error(`[ConfigRegistry] Failed to sync ${type} registry:`, err.message);
753
+ }
754
+ }
755
+ }
756
+
757
+ module.exports = {
758
+ ConfigRegistryService,
759
+ CONFIG_TYPES,
760
+ CONFIGS_DIR,
761
+ REGISTRY_FILE
762
+ };