contextos-agents 2.1.0 → 2.2.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 (192) hide show
  1. package/.agents/adapters/aider/export.js +2 -2
  2. package/.agents/adapters/claude/export.js +53 -2
  3. package/.agents/adapters/drift-detector.js +6 -3
  4. package/.agents/adapters/pure-compiler.js +18 -6
  5. package/.agents/ctx.js +13 -8
  6. package/.agents/plugins.js +347 -26
  7. package/.agents/profiles.js +32 -11
  8. package/README.md +38 -3
  9. package/bin/commands/hook.js +50 -12
  10. package/bin/commands/scan.js +10 -3
  11. package/bin/index.js +165 -53
  12. package/bin/lib/git-snapshot.js +70 -43
  13. package/bin/lib/scan.js +108 -27
  14. package/bin/lib/ui.js +140 -0
  15. package/catalog/skills/adapters/EXAMPLES.md +19 -0
  16. package/catalog/skills/adapters/SKILL.md +101 -0
  17. package/catalog/skills/adapters/TROUBLESHOOTING.md +7 -0
  18. package/catalog/skills/adapters/VALIDATION.json +12 -0
  19. package/catalog/skills/adapters/skill.yaml +13 -0
  20. package/catalog/skills/api-design/EXAMPLES.md +91 -0
  21. package/catalog/skills/api-design/SKILL.md +63 -0
  22. package/catalog/skills/api-design/TROUBLESHOOTING.md +54 -0
  23. package/catalog/skills/api-design/VALIDATION.json +11 -0
  24. package/catalog/skills/api-design/skill.yaml +14 -0
  25. package/catalog/skills/architecture-diagrams/SKILL.md +108 -0
  26. package/catalog/skills/architecture-diagrams/VALIDATION.json +12 -0
  27. package/catalog/skills/architecture-diagrams/skill.yaml +9 -0
  28. package/catalog/skills/brutalist-design/EXAMPLES.md +59 -0
  29. package/catalog/skills/brutalist-design/SKILL.md +150 -0
  30. package/catalog/skills/brutalist-design/VALIDATION.json +12 -0
  31. package/catalog/skills/brutalist-design/skill.yaml +10 -0
  32. package/catalog/skills/ci-cd/EXAMPLES.md +79 -0
  33. package/catalog/skills/ci-cd/SKILL.md +69 -0
  34. package/catalog/skills/ci-cd/TROUBLESHOOTING.md +52 -0
  35. package/catalog/skills/ci-cd/VALIDATION.json +11 -0
  36. package/catalog/skills/ci-cd/skill.yaml +13 -0
  37. package/catalog/skills/database/EXAMPLES.md +74 -0
  38. package/catalog/skills/database/SKILL.md +101 -0
  39. package/catalog/skills/database/TROUBLESHOOTING.md +18 -0
  40. package/catalog/skills/database/VALIDATION.json +11 -0
  41. package/catalog/skills/database/skill.yaml +14 -0
  42. package/catalog/skills/ddd/EXAMPLES.md +42 -0
  43. package/catalog/skills/ddd/SKILL.md +247 -0
  44. package/catalog/skills/ddd/TROUBLESHOOTING.md +19 -0
  45. package/catalog/skills/ddd/VALIDATION.json +12 -0
  46. package/catalog/skills/ddd/skill.yaml +14 -0
  47. package/catalog/skills/decisions/EXAMPLES.md +35 -0
  48. package/catalog/skills/decisions/SKILL.md +90 -0
  49. package/catalog/skills/decisions/TROUBLESHOOTING.md +13 -0
  50. package/catalog/skills/decisions/VALIDATION.json +12 -0
  51. package/catalog/skills/decisions/skill.yaml +13 -0
  52. package/catalog/skills/docker/EXAMPLES.md +56 -0
  53. package/catalog/skills/docker/SKILL.md +169 -0
  54. package/catalog/skills/docker/TROUBLESHOOTING.md +18 -0
  55. package/catalog/skills/docker/VALIDATION.json +11 -0
  56. package/catalog/skills/docker/skill.yaml +13 -0
  57. package/catalog/skills/fastapi/EXAMPLES.md +36 -0
  58. package/catalog/skills/fastapi/SKILL.md +171 -0
  59. package/catalog/skills/fastapi/TROUBLESHOOTING.md +19 -0
  60. package/catalog/skills/fastapi/VALIDATION.json +12 -0
  61. package/catalog/skills/fastapi/skill.yaml +14 -0
  62. package/catalog/skills/generators/EXAMPLES.md +19 -0
  63. package/catalog/skills/generators/SKILL.md +110 -0
  64. package/catalog/skills/generators/TROUBLESHOOTING.md +7 -0
  65. package/catalog/skills/generators/VALIDATION.json +12 -0
  66. package/catalog/skills/generators/skill.yaml +22 -0
  67. package/catalog/skills/generators/templates/API.md +77 -0
  68. package/catalog/skills/generators/templates/ARCHITECTURE.md +70 -0
  69. package/catalog/skills/generators/templates/DATABASE.md +42 -0
  70. package/catalog/skills/generators/templates/DECISION.md +46 -0
  71. package/catalog/skills/generators/templates/PRD.md +67 -0
  72. package/catalog/skills/generators/templates/PROJECT_GRAPH.md +56 -0
  73. package/catalog/skills/generators/templates/ROADMAP.md +51 -0
  74. package/catalog/skills/generators/templates/TASKS.md +43 -0
  75. package/catalog/skills/generators/templates/UI.md +73 -0
  76. package/catalog/skills/graphify/EXAMPLES.md +73 -0
  77. package/catalog/skills/graphify/SKILL.md +130 -0
  78. package/catalog/skills/graphify/VALIDATION.json +12 -0
  79. package/catalog/skills/graphify/skill.yaml +13 -0
  80. package/catalog/skills/impeccable-design/EXAMPLES.md +26 -0
  81. package/catalog/skills/impeccable-design/SKILL.md +201 -0
  82. package/catalog/skills/impeccable-design/TROUBLESHOOTING.md +19 -0
  83. package/catalog/skills/impeccable-design/VALIDATION.json +12 -0
  84. package/catalog/skills/impeccable-design/skill.yaml +15 -0
  85. package/catalog/skills/interview-me/SKILL.md +97 -0
  86. package/catalog/skills/interview-me/VALIDATION.json +12 -0
  87. package/catalog/skills/interview-me/skill.yaml +9 -0
  88. package/catalog/skills/microservices/EXAMPLES.md +38 -0
  89. package/catalog/skills/microservices/SKILL.md +164 -0
  90. package/catalog/skills/microservices/TROUBLESHOOTING.md +19 -0
  91. package/catalog/skills/microservices/VALIDATION.json +12 -0
  92. package/catalog/skills/microservices/skill.yaml +14 -0
  93. package/catalog/skills/minimalist-design/EXAMPLES.md +58 -0
  94. package/catalog/skills/minimalist-design/SKILL.md +113 -0
  95. package/catalog/skills/minimalist-design/VALIDATION.json +12 -0
  96. package/catalog/skills/minimalist-design/skill.yaml +10 -0
  97. package/catalog/skills/nestjs/EXAMPLES.md +40 -0
  98. package/catalog/skills/nestjs/SKILL.md +139 -0
  99. package/catalog/skills/nestjs/TROUBLESHOOTING.md +19 -0
  100. package/catalog/skills/nestjs/VALIDATION.json +12 -0
  101. package/catalog/skills/nestjs/skill.yaml +14 -0
  102. package/catalog/skills/nextjs/EXAMPLES.md +40 -0
  103. package/catalog/skills/nextjs/SKILL.md +163 -0
  104. package/catalog/skills/nextjs/TROUBLESHOOTING.md +19 -0
  105. package/catalog/skills/nextjs/VALIDATION.json +12 -0
  106. package/catalog/skills/nextjs/skill.yaml +14 -0
  107. package/catalog/skills/node/EXAMPLES.md +80 -0
  108. package/catalog/skills/node/SKILL.md +128 -0
  109. package/catalog/skills/node/TROUBLESHOOTING.md +19 -0
  110. package/catalog/skills/node/VALIDATION.json +12 -0
  111. package/catalog/skills/node/skill.yaml +14 -0
  112. package/catalog/skills/performance/EXAMPLES.md +30 -0
  113. package/catalog/skills/performance/SKILL.md +75 -0
  114. package/catalog/skills/performance/TROUBLESHOOTING.md +19 -0
  115. package/catalog/skills/performance/VALIDATION.json +12 -0
  116. package/catalog/skills/performance/skill.yaml +14 -0
  117. package/catalog/skills/react/EXAMPLES.md +79 -0
  118. package/catalog/skills/react/SKILL.md +132 -0
  119. package/catalog/skills/react/TROUBLESHOOTING.md +19 -0
  120. package/catalog/skills/react/VALIDATION.json +12 -0
  121. package/catalog/skills/react/skill.yaml +14 -0
  122. package/catalog/skills/react-best-practices/SKILL.md +158 -0
  123. package/catalog/skills/react-best-practices/VALIDATION.json +12 -0
  124. package/catalog/skills/react-best-practices/skill.yaml +13 -0
  125. package/catalog/skills/redesign-audit/SKILL.md +117 -0
  126. package/catalog/skills/redesign-audit/VALIDATION.json +12 -0
  127. package/catalog/skills/redesign-audit/skill.yaml +9 -0
  128. package/catalog/skills/security-audit/EXAMPLES.md +79 -0
  129. package/catalog/skills/security-audit/SKILL.md +91 -0
  130. package/catalog/skills/security-audit/TROUBLESHOOTING.md +46 -0
  131. package/catalog/skills/security-audit/VALIDATION.json +11 -0
  132. package/catalog/skills/security-audit/skill.yaml +14 -0
  133. package/catalog/skills/soft-design/EXAMPLES.md +51 -0
  134. package/catalog/skills/soft-design/SKILL.md +108 -0
  135. package/catalog/skills/soft-design/VALIDATION.json +12 -0
  136. package/catalog/skills/soft-design/skill.yaml +10 -0
  137. package/catalog/skills/state-management/EXAMPLES.md +56 -0
  138. package/catalog/skills/state-management/SKILL.md +168 -0
  139. package/catalog/skills/state-management/TROUBLESHOOTING.md +18 -0
  140. package/catalog/skills/state-management/VALIDATION.json +11 -0
  141. package/catalog/skills/state-management/skill.yaml +14 -0
  142. package/catalog/skills/subagent-orchestrator/SKILL.md +117 -0
  143. package/catalog/skills/subagent-orchestrator/VALIDATION.json +12 -0
  144. package/catalog/skills/subagent-orchestrator/skill.yaml +9 -0
  145. package/catalog/skills/system-design/EXAMPLES.md +75 -0
  146. package/catalog/skills/system-design/SKILL.md +419 -0
  147. package/catalog/skills/system-design/TROUBLESHOOTING.md +19 -0
  148. package/catalog/skills/system-design/VALIDATION.json +12 -0
  149. package/catalog/skills/system-design/skill.yaml +14 -0
  150. package/catalog/skills/terraform/EXAMPLES.md +74 -0
  151. package/catalog/skills/terraform/SKILL.md +55 -0
  152. package/catalog/skills/terraform/TROUBLESHOOTING.md +53 -0
  153. package/catalog/skills/terraform/VALIDATION.json +11 -0
  154. package/catalog/skills/terraform/skill.yaml +14 -0
  155. package/catalog/skills/testing/EXAMPLES.md +122 -0
  156. package/catalog/skills/testing/SKILL.md +70 -0
  157. package/catalog/skills/testing/TROUBLESHOOTING.md +18 -0
  158. package/catalog/skills/testing/VALIDATION.json +11 -0
  159. package/catalog/skills/testing/skill.yaml +14 -0
  160. package/catalog/skills/typescript/EXAMPLES.md +64 -0
  161. package/catalog/skills/typescript/SKILL.md +112 -0
  162. package/catalog/skills/typescript/TROUBLESHOOTING.md +19 -0
  163. package/catalog/skills/typescript/VALIDATION.json +12 -0
  164. package/catalog/skills/typescript/skill.yaml +14 -0
  165. package/catalog/skills/ui-design/EXAMPLES.md +21 -0
  166. package/catalog/skills/ui-design/SKILL.md +124 -0
  167. package/catalog/skills/ui-design/TROUBLESHOOTING.md +19 -0
  168. package/catalog/skills/ui-design/VALIDATION.json +12 -0
  169. package/catalog/skills/ui-design/skill.yaml +16 -0
  170. package/catalog/skills/ui-ux-pro/EXAMPLES.md +62 -0
  171. package/catalog/skills/ui-ux-pro/SKILL.md +418 -0
  172. package/catalog/skills/ui-ux-pro/TROUBLESHOOTING.md +19 -0
  173. package/catalog/skills/ui-ux-pro/VALIDATION.json +12 -0
  174. package/catalog/skills/ui-ux-pro/skill.yaml +14 -0
  175. package/catalog/skills/ux-design/EXAMPLES.md +36 -0
  176. package/catalog/skills/ux-design/SKILL.md +116 -0
  177. package/catalog/skills/ux-design/TROUBLESHOOTING.md +19 -0
  178. package/catalog/skills/ux-design/VALIDATION.json +12 -0
  179. package/catalog/skills/ux-design/skill.yaml +16 -0
  180. package/catalog/skills/vercel-optimize/SKILL.md +83 -0
  181. package/catalog/skills/vercel-optimize/VALIDATION.json +12 -0
  182. package/catalog/skills/vercel-optimize/scripts/collect-signals.mjs +131 -0
  183. package/catalog/skills/vercel-optimize/scripts/gate-investigations.mjs +142 -0
  184. package/catalog/skills/vercel-optimize/scripts/merge-signals.mjs +143 -0
  185. package/catalog/skills/vercel-optimize/scripts/scan-codebase.mjs +174 -0
  186. package/catalog/skills/vercel-optimize/skill.yaml +15 -0
  187. package/catalog/skills/web-accessibility/EXAMPLES.md +39 -0
  188. package/catalog/skills/web-accessibility/SKILL.md +151 -0
  189. package/catalog/skills/web-accessibility/TROUBLESHOOTING.md +19 -0
  190. package/catalog/skills/web-accessibility/VALIDATION.json +12 -0
  191. package/catalog/skills/web-accessibility/skill.yaml +14 -0
  192. package/package.json +3 -2
@@ -26,7 +26,7 @@ const https = require('https');
26
26
  const crypto = require('crypto');
27
27
  const { execFileSync } = require('child_process');
28
28
  const { ProjectMutationLock } = require('./filesystem/project-lock.js');
29
- const JournaledTransaction = require('./filesystem/journaled-transaction.js');
29
+ const { JournaledTransaction } = require('./filesystem/journaled-transaction.js');
30
30
  const {
31
31
  calculateTreeDigest,
32
32
  validateArchiveEntry,
@@ -47,6 +47,25 @@ const REGISTRY_URL = 'https://raw.githubusercontent.com/kok-o/contextos-agents
47
47
  const MAX_DOWNLOAD_BYTES = 5 * 1024 * 1024;
48
48
  const SAFE_SKILL_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
49
49
 
50
+ function getCatalogDir() {
51
+ if (process.env.CONTEXTOS_CATALOG_DIR && fs.existsSync(process.env.CONTEXTOS_CATALOG_DIR)) {
52
+ return process.env.CONTEXTOS_CATALOG_DIR;
53
+ }
54
+ const localAgentsCatalog = path.join(__dirname, 'catalog', 'skills');
55
+ if (fs.existsSync(localAgentsCatalog)) return localAgentsCatalog;
56
+
57
+ const relativeCatalog = path.join(path.resolve(__dirname, '..'), 'catalog', 'skills');
58
+ if (fs.existsSync(relativeCatalog)) return relativeCatalog;
59
+
60
+ try {
61
+ const pkgPath = require.resolve('contextos-agents/package.json', { paths: [__dirname, process.cwd()] });
62
+ const pkgCatalog = path.join(path.dirname(pkgPath), 'catalog', 'skills');
63
+ if (fs.existsSync(pkgCatalog)) return pkgCatalog;
64
+ } catch {}
65
+
66
+ return null;
67
+ }
68
+
50
69
  // ── ANSI helpers ──────────────────────────────────────────────────────────────
51
70
  const NO_COLOR = process.env.NO_COLOR || !process.stdout.isTTY;
52
71
  const c = {
@@ -284,11 +303,49 @@ function isValidNpmPackage(name) {
284
303
  return /^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+$/.test(name);
285
304
  }
286
305
 
287
- function parseRef(ref) {
306
+ function parseRef(ref, projectRoot = null) {
288
307
  if (typeof ref !== 'string' || !ref.trim()) {
289
308
  throw new Error('A plugin reference is required');
290
309
  }
291
310
 
311
+ // 1. Built-in package catalog skills take strict precedence for catalog names.
312
+ // Consumer project local files can never spoof or hijack package catalog skills.
313
+ const pkgCatalogPath = path.join(getCatalogDir(), ref);
314
+ if (fs.existsSync(pkgCatalogPath) && fs.existsSync(path.join(pkgCatalogPath, 'SKILL.md'))) {
315
+ return {
316
+ type: 'catalog',
317
+ name: ref,
318
+ path: pkgCatalogPath,
319
+ isPinned: true,
320
+ raw: ref,
321
+ };
322
+ }
323
+
324
+ // 2. Explicit local references (must start with 'local:', './', '../', or 'file:')
325
+ if (ref.startsWith('local:') || ref.startsWith('./') || ref.startsWith('../') || ref.startsWith('file:')) {
326
+ const rawTarget = ref.startsWith('local:')
327
+ ? ref.slice(6)
328
+ : (ref.startsWith('file:') ? ref.slice(5) : ref);
329
+
330
+ const baseRoot = projectRoot || process.cwd();
331
+ let localDir = path.resolve(baseRoot, rawTarget);
332
+ if (!fs.existsSync(localDir)) {
333
+ localDir = path.join(baseRoot, 'catalog', 'skills', rawTarget);
334
+ }
335
+
336
+ if (fs.existsSync(localDir) && fs.existsSync(path.join(localDir, 'SKILL.md'))) {
337
+ const skillName = path.basename(localDir);
338
+ return {
339
+ type: 'catalog',
340
+ name: skillName,
341
+ path: localDir,
342
+ isPinned: true,
343
+ raw: ref,
344
+ };
345
+ }
346
+ throw new Error(`Local skill not found at: '${ref}'`);
347
+ }
348
+
292
349
  // Scoped packages contain a slash but are npm packages, not GitHub refs.
293
350
  if (ref.startsWith('@')) {
294
351
  if (!isValidNpmPackage(ref)) {
@@ -529,6 +586,62 @@ function installFromNpm(descriptor, skillName, dryRun, checksum, forceUnsafe = f
529
586
  return sha256;
530
587
  }
531
588
 
589
+ // ── Catalog installer ─────────────────────────────────────────────────────────
590
+ function installFromCatalog(descriptor, skillName, dryRun, checksum, forceUnsafe = false, tx = null) {
591
+ const sourceDir = descriptor.path;
592
+ const targetDir = path.join(PLUGINS_DIR, skillName);
593
+ const relTargetDir = toPosix(path.relative(ROOT, targetDir));
594
+
595
+ if (dryRun) {
596
+ console.log(c.dim(` [DRY-RUN] Would copy catalog skill from: ${sourceDir}`));
597
+ return 'sha256:0000000000000000000000000000000000000000000000000000000000000000';
598
+ }
599
+
600
+ function copyRecursive(src, dest, relDest) {
601
+ if (!fs.existsSync(dest)) {
602
+ fs.mkdirSync(dest, { recursive: true });
603
+ }
604
+ const entries = fs.readdirSync(src, { withFileTypes: true });
605
+ for (const entry of entries) {
606
+ const srcPath = path.join(src, entry.name);
607
+ const destPath = path.join(dest, entry.name);
608
+ const relPath = toPosix(path.join(relDest, entry.name));
609
+
610
+ if (entry.isDirectory()) {
611
+ copyRecursive(srcPath, destPath, relPath);
612
+ } else {
613
+ const content = fs.readFileSync(srcPath, 'utf8');
614
+ if (tx) {
615
+ tx.stageWrite(relPath, content);
616
+ } else {
617
+ fs.writeFileSync(destPath, content, 'utf8');
618
+ }
619
+ }
620
+ }
621
+ }
622
+
623
+ copyRecursive(sourceDir, targetDir, relTargetDir);
624
+
625
+ const primarySkillMd = path.join(sourceDir, 'SKILL.md');
626
+ const sha256 = fs.existsSync(primarySkillMd) ? crypto.createHash('sha256').update(fs.readFileSync(primarySkillMd)).digest('hex') : null;
627
+
628
+ const sourceMeta = {
629
+ source: 'catalog',
630
+ name: skillName,
631
+ path: toPosix(path.relative(ROOT, sourceDir)),
632
+ sha256,
633
+ installedAt: new Date().toISOString(),
634
+ };
635
+ if (tx) {
636
+ tx.stageWrite(`${relTargetDir}/.source`, JSON.stringify(sourceMeta, null, 2) + '\n');
637
+ } else {
638
+ fs.writeFileSync(path.join(targetDir, '.source'), JSON.stringify(sourceMeta, null, 2) + '\n');
639
+ }
640
+
641
+ console.log(c.green(` ✓ Installed from built-in catalog: ${skillName}`));
642
+ return sha256;
643
+ }
644
+
532
645
  // ── Derive skill name from ref ────────────────────────────────────────────────
533
646
  function deriveSkillName(ref) {
534
647
  // username/repo/path/to/my-skill → my-skill
@@ -542,9 +655,137 @@ function isSafeSkillName(skillName) {
542
655
  return typeof skillName === 'string' && SAFE_SKILL_NAME.test(skillName);
543
656
  }
544
657
 
545
- // ═════════════════════════════════════════════════════════════════════════════
546
- // COMMANDS
547
- // ═════════════════════════════════════════════════════════════════════════════
658
+ function findClosestSkill(target, candidates) {
659
+ let closest = null;
660
+ let minDistance = Infinity;
661
+ for (const item of candidates) {
662
+ if (item === target) return item;
663
+ if (item.includes(target) || target.includes(item)) return item;
664
+ const dist = levenshtein(target, item);
665
+ if (dist < minDistance && dist <= 3) {
666
+ minDistance = dist;
667
+ closest = item;
668
+ }
669
+ }
670
+ return closest;
671
+ }
672
+
673
+ function levenshtein(a, b) {
674
+ const m = a.length, n = b.length;
675
+ const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
676
+ for (let i = 0; i <= m; i++) d[i][0] = i;
677
+ for (let j = 0; j <= n; j++) d[0][j] = j;
678
+ for (let i = 1; i <= m; i++) {
679
+ for (let j = 1; j <= n; j++) {
680
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
681
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
682
+ }
683
+ }
684
+ return d[m][n];
685
+ }
686
+
687
+ /**
688
+ * Batch install all available catalog skills.
689
+ */
690
+ async function addAllFromCatalog(options = {}) {
691
+ const dryRun = Boolean(options.dryRun || options['dry-run']);
692
+ const force = Boolean(options.force);
693
+ const catalogDir = getCatalogDir();
694
+ if (!fs.existsSync(catalogDir)) {
695
+ throw new Error('Catalog directory not found in package');
696
+ }
697
+
698
+ const catalogSkills = fs.readdirSync(catalogDir).filter(n => {
699
+ const full = path.join(catalogDir, n);
700
+ return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'SKILL.md'));
701
+ });
702
+
703
+ console.log('');
704
+ console.log(c.bold(`Batch installing ${catalogSkills.length} catalog skills...`));
705
+
706
+ const lock = readLock();
707
+ const existingNames = new Set(lock.plugins.map(p => p.name));
708
+
709
+ const projectLock = new ProjectMutationLock(ROOT);
710
+ const tx = new JournaledTransaction(ROOT);
711
+ let lockToken = null;
712
+
713
+ let installedCount = 0;
714
+ let skippedCount = 0;
715
+ const newlyInstalled = [];
716
+
717
+ try {
718
+ lockToken = projectLock.acquire({ command: 'skill:add-all' });
719
+
720
+ for (const skillName of catalogSkills) {
721
+ if (existingNames.has(skillName) && !force) {
722
+ skippedCount++;
723
+ continue;
724
+ }
725
+
726
+ const descriptor = {
727
+ type: 'catalog',
728
+ name: skillName,
729
+ path: path.join(catalogDir, skillName),
730
+ isPinned: true,
731
+ raw: skillName,
732
+ };
733
+
734
+ const installedSha256 = installFromCatalog(descriptor, skillName, dryRun, null, false, tx);
735
+
736
+ if (!dryRun) {
737
+ const idx = lock.plugins.findIndex(p => p.name === skillName);
738
+ const entry = {
739
+ name: skillName,
740
+ ref: skillName,
741
+ type: 'catalog',
742
+ sha256: installedSha256,
743
+ installedAt: new Date().toISOString(),
744
+ };
745
+ if (idx >= 0) lock.plugins[idx] = entry;
746
+ else lock.plugins.push(entry);
747
+ newlyInstalled.push(skillName);
748
+ }
749
+ installedCount++;
750
+ }
751
+
752
+ if (!dryRun) {
753
+ writeLock(lock, tx);
754
+ tx.commit();
755
+
756
+ for (const skillName of newlyInstalled) {
757
+ const targetDir = path.join(PLUGINS_DIR, skillName);
758
+ if (fs.existsSync(targetDir)) {
759
+ const digestResult = calculateTreeDigest(targetDir);
760
+ const entry = lock.plugins.find(p => p.name === skillName);
761
+ if (entry) entry.treeDigest = digestResult.treeDigest;
762
+ }
763
+ }
764
+ writeLock(lock);
765
+ }
766
+
767
+ console.log('');
768
+ if (dryRun) {
769
+ console.log(c.dim(`[DRY-RUN] Would install ${installedCount} skills (${skippedCount} already installed - skipped).`));
770
+ } else {
771
+ console.log(c.green(c.bold(`✓ Batch install complete: ${installedCount} catalog skills installed.`)));
772
+ if (skippedCount > 0) {
773
+ console.log(c.dim(` (${skippedCount} skills already installed - skipped)`));
774
+ }
775
+ console.log('');
776
+ console.log(' Next steps:');
777
+ console.log(' node .agents/ctx.js export all # recompile with new skills');
778
+ console.log(' node .agents/ctx.js validate # verify consistency');
779
+ }
780
+
781
+ return { installedCount, skippedCount };
782
+ } catch (err) {
783
+ console.error(c.red(`\n[ERROR] Batch installation failed: ${err.message}`));
784
+ process.exit(1);
785
+ } finally {
786
+ if (lockToken) projectLock.release(lockToken);
787
+ }
788
+ }
548
789
 
549
790
  /**
550
791
  * skill add <ref> [--dry-run] [--checksum <sha256>] [--force-unsafe-prompts]
@@ -556,6 +797,10 @@ async function add(ref, options = {}) {
556
797
  const allowFloating = Boolean(options.allowFloating || options['allow-floating']);
557
798
  const force = Boolean(options.force);
558
799
 
800
+ if (ref === '--all' || ref === 'all' || options.all) {
801
+ return await addAllFromCatalog(options);
802
+ }
803
+
559
804
  if (!ref) {
560
805
  console.error(c.red('Usage: ctx.js skill add <ref> [--checksum <sha256>]'));
561
806
  console.error(' ref can be: username/repo, username/repo/path/to/skill, or npm-package-name');
@@ -569,8 +814,38 @@ async function add(ref, options = {}) {
569
814
  throw new Error(`Invalid plugin name derived from reference: '${skillName}'`);
570
815
  }
571
816
 
572
- // Supply-chain source pinning check (Wave 6)
573
- if (!dryRun) {
817
+ // 1. Check for conflict with builtin skills first
818
+ const builtinPath = path.join(CORE_SKILLS, skillName);
819
+ if (fs.existsSync(builtinPath)) {
820
+ console.error(c.red(`\n[ERROR] '${skillName}' conflicts with a built-in skill.`));
821
+ console.error(c.red(' Built-in skills cannot be overridden via --add-skill.'));
822
+ console.error(c.dim(` To customize a built-in skill for your project, run: contextos skill override ${skillName}`));
823
+ process.exit(1);
824
+ }
825
+
826
+ // 2. If single word not in catalog, check for typo suggestion
827
+ if (descriptor.type !== 'catalog' && !ref.includes('/') && !ref.includes('@')) {
828
+ const catalogDir = getCatalogDir();
829
+ if (fs.existsSync(catalogDir)) {
830
+ const catalogSkills = fs.readdirSync(catalogDir).filter(n => {
831
+ try {
832
+ return fs.statSync(path.join(catalogDir, n)).isDirectory();
833
+ } catch {
834
+ return false;
835
+ }
836
+ });
837
+ const suggestion = findClosestSkill(skillName, catalogSkills);
838
+ if (suggestion) {
839
+ console.error(c.red(`\n[ERROR] Skill '${skillName}' not found in catalog.`));
840
+ console.log(c.yellow(` Did you mean: "${suggestion}"?`));
841
+ console.log(c.dim(' Run "contextos skill list --available" to see all catalog skills.'));
842
+ process.exit(1);
843
+ }
844
+ }
845
+ }
846
+
847
+ // 3. Supply-chain source pinning check (Wave 6)
848
+ if (!dryRun && descriptor.type !== 'catalog') {
574
849
  const pinning = validatePluginPinning(
575
850
  {
576
851
  type: descriptor.type,
@@ -594,15 +869,6 @@ async function add(ref, options = {}) {
594
869
  console.log(c.bold(`Installing skill: ${c.cyan(skillName)}`));
595
870
  console.log(c.dim(` Source: ${ref} (${descriptor.type})`));
596
871
 
597
- // Check for conflict with builtin skills
598
- const builtinPath = path.join(CORE_SKILLS, skillName);
599
- if (fs.existsSync(builtinPath)) {
600
- console.error(c.red(`\n[ERROR] '${skillName}' conflicts with a built-in skill.`));
601
- console.error(c.red(' Built-in skills cannot be overridden via --add-skill.'));
602
- console.error(c.dim(' To customize a built-in skill, fork the repo and submit a PR.'));
603
- process.exit(1);
604
- }
605
-
606
872
  // Check if already installed
607
873
  const lock = readLock();
608
874
  const existing = lock.plugins.find(p => p.name === skillName);
@@ -637,7 +903,9 @@ async function add(ref, options = {}) {
637
903
  lockToken = projectLock.acquire({ command: 'skill:add' });
638
904
 
639
905
  let installedSha256 = null;
640
- if (descriptor.type === 'github') {
906
+ if (descriptor.type === 'catalog') {
907
+ installedSha256 = installFromCatalog(descriptor, skillName, dryRun, checksum, forceUnsafe, tx);
908
+ } else if (descriptor.type === 'github') {
641
909
  installedSha256 = await installFromGitHub(descriptor, skillName, dryRun, checksum, forceUnsafe, tx);
642
910
  } else {
643
911
  installedSha256 = installFromNpm(descriptor, skillName, dryRun, checksum, forceUnsafe, tx);
@@ -683,6 +951,21 @@ async function add(ref, options = {}) {
683
951
  // We don't need to do anything since it wasn't committed
684
952
  }
685
953
  console.error(c.red(`\n[ERROR] Failed to install '${skillName}': ${err.message}`));
954
+ const catalogDir = getCatalogDir();
955
+ if (fs.existsSync(catalogDir)) {
956
+ const catalogSkills = fs.readdirSync(catalogDir).filter(n => {
957
+ try {
958
+ return fs.statSync(path.join(catalogDir, n)).isDirectory();
959
+ } catch {
960
+ return false;
961
+ }
962
+ });
963
+ const suggestion = findClosestSkill(skillName, catalogSkills);
964
+ if (suggestion) {
965
+ console.log(c.yellow(` Did you mean: "${suggestion}"?`));
966
+ console.log(c.dim(' Run "contextos skill list --available" to see all catalog skills.'));
967
+ }
968
+ }
686
969
  process.exit(1);
687
970
  } finally {
688
971
  if (lockToken) projectLock.release(lockToken);
@@ -802,25 +1085,46 @@ function remove(skillName) {
802
1085
  }
803
1086
 
804
1087
  /**
805
- * skill list
1088
+ * skill list [options]
806
1089
  */
807
- function list() {
1090
+ function list(options = {}) {
1091
+ const showAvailable = Boolean(options.available || options['--available'] || options.all);
808
1092
  const lock = readLock();
809
1093
 
810
1094
  // Builtin skills
811
1095
  const builtins = fs.existsSync(CORE_SKILLS)
812
- ? fs.readdirSync(CORE_SKILLS).filter(n => fs.statSync(path.join(CORE_SKILLS, n)).isDirectory())
1096
+ ? fs.readdirSync(CORE_SKILLS).filter(n => {
1097
+ try {
1098
+ return fs.statSync(path.join(CORE_SKILLS, n)).isDirectory();
1099
+ } catch {
1100
+ return false;
1101
+ }
1102
+ })
813
1103
  : [];
814
1104
 
815
1105
  // Plugin skills
816
1106
  const plugins = lock.plugins;
817
1107
 
1108
+ // Catalog skills
1109
+ const catalogDir = getCatalogDir();
1110
+ const catalogSkills = fs.existsSync(catalogDir)
1111
+ ? fs.readdirSync(catalogDir).filter(n => {
1112
+ try {
1113
+ return fs.statSync(path.join(catalogDir, n)).isDirectory();
1114
+ } catch {
1115
+ return false;
1116
+ }
1117
+ })
1118
+ : [];
1119
+ const installedNames = new Set(plugins.map(p => p.name));
1120
+ const availableCatalog = catalogSkills.filter(n => !installedNames.has(n));
1121
+
818
1122
  console.log('');
819
1123
  console.log(c.bold('ContextOS Skills'));
820
1124
  console.log('');
821
1125
 
822
1126
  // Built-ins
823
- console.log(c.bold(` Built-in (${builtins.length})`));
1127
+ console.log(c.bold(` Built-in Core (${builtins.length})`));
824
1128
  for (const name of builtins) {
825
1129
  console.log(` ${c.cyan('●')} ${name} ${c.dim('(core)')}`);
826
1130
  }
@@ -828,22 +1132,39 @@ function list() {
828
1132
  // Plugins
829
1133
  console.log('');
830
1134
  if (plugins.length === 0) {
831
- console.log(c.bold(' Plugins (0)'));
832
- console.log(c.dim(' No plugins installed yet.'));
833
- console.log(c.dim(' Install one: node .agents/ctx.js skill add username/my-skill'));
1135
+ console.log(c.bold(' Installed Plugins & Domain Skills (0)'));
1136
+ console.log(c.dim(' No domain skills or plugins installed yet.'));
1137
+ console.log(c.dim(' Install one: contextos skill add <name> (or --all)'));
834
1138
  } else {
835
- console.log(c.bold(` Plugins (${plugins.length})`));
1139
+ console.log(c.bold(` Installed Plugins & Domain Skills (${plugins.length})`));
836
1140
  for (const p of plugins) {
837
1141
  const when = p.installedAt ? new Date(p.installedAt).toLocaleDateString() : '?';
838
1142
  console.log(` ${c.green('●')} ${p.name} ${c.dim(`← ${p.ref} (installed ${when})`)}`);
839
1143
  }
840
1144
  }
841
1145
 
1146
+ // Available in Catalog
1147
+ console.log('');
1148
+ if (showAvailable || availableCatalog.length > 0) {
1149
+ console.log(c.bold(` Available in Catalog (${availableCatalog.length})`));
1150
+ if (showAvailable) {
1151
+ for (const name of availableCatalog) {
1152
+ console.log(` ${c.dim('○')} ${name}`);
1153
+ }
1154
+ console.log('');
1155
+ console.log(c.dim(' Install with: contextos skill add <name> (or contextos skill add --all)'));
1156
+ } else {
1157
+ console.log(c.dim(` ${availableCatalog.length} domain skills available in local catalog.`));
1158
+ console.log(c.dim(' Run "contextos skill list --available" to see all, or install with:'));
1159
+ console.log(c.dim(' contextos skill add <name> | contextos skill add --all'));
1160
+ }
1161
+ }
1162
+
842
1163
  console.log('');
843
1164
  }
844
1165
 
845
1166
  /**
846
- * skill search <query> — searches the hosted registry.json
1167
+ * skill search <query> - searches the hosted registry.json
847
1168
  */
848
1169
  async function search(query) {
849
1170
  console.log(c.dim(`\nFetching registry from: ${REGISTRY_URL}\n`));
@@ -298,14 +298,26 @@ function validateProfile(profile, registry = null) {
298
298
  }
299
299
 
300
300
  /**
301
- * Lists all available project profiles defined in core/profiles.
301
+ * Lists all available project profiles defined in core/profiles and optionally consumer project.
302
+ * Searches built-in profiles in packageRoot, and if projectRoot is supplied, searches
303
+ * consumer project .agents/profiles, .agents/core/profiles, and profiles directories.
302
304
  *
305
+ * @param {string|null} [projectRoot=null] - Optional consumer project root
303
306
  * @returns {ProfileConfig[]} Array of profile configurations
304
307
  */
305
- function listProfiles() {
308
+ function listProfiles(projectRoot = null) {
306
309
  const profiles = [];
307
310
  const searchDirs = [PROFILES_DIR, path.join(AGENTS_DIR, 'catalog', 'presets')];
308
311
 
312
+ if (projectRoot) {
313
+ const absConsumer = path.resolve(projectRoot);
314
+ searchDirs.push(
315
+ path.join(absConsumer, '.agents', 'profiles'),
316
+ path.join(absConsumer, '.agents', 'core', 'profiles'),
317
+ path.join(absConsumer, 'profiles')
318
+ );
319
+ }
320
+
309
321
  for (const dir of searchDirs) {
310
322
  if (!fs.existsSync(dir)) continue;
311
323
  const files = fs.readdirSync(dir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
@@ -314,22 +326,31 @@ function listProfiles() {
314
326
  const parsed = parseYamlProfile(content);
315
327
  if (!parsed.id) parsed.id = path.basename(file, path.extname(file));
316
328
  if (!parsed.name) parsed.name = parsed.id;
317
- profiles.push(parsed);
329
+
330
+ // Consumer profiles can override or extend built-in profiles; deduplicate by id
331
+ const normId = parsed.id.toLowerCase();
332
+ const existingIdx = profiles.findIndex(p => p.id.toLowerCase() === normId);
333
+ if (existingIdx !== -1) {
334
+ profiles[existingIdx] = parsed;
335
+ } else {
336
+ profiles.push(parsed);
337
+ }
318
338
  }
319
339
  }
320
340
  return profiles;
321
341
  }
322
342
 
323
343
  /**
324
- * Finds a profile by its slug ID or title.
344
+ * Finds a profile by its slug ID or title across package and consumer roots.
325
345
  *
326
346
  * @param {string} name - Profile identifier or name
347
+ * @param {string|null} [projectRoot=null] - Optional consumer project root
327
348
  * @returns {ProfileConfig|null} Found profile or null
328
349
  */
329
- function getProfile(name) {
350
+ function getProfile(name, projectRoot = null) {
330
351
  if (!name) return null;
331
352
  const clean = name.toLowerCase().trim();
332
- const all = listProfiles();
353
+ const all = listProfiles(projectRoot);
333
354
  return all.find(p => p.id.toLowerCase() === clean || p.name.toLowerCase() === clean) || null;
334
355
  }
335
356
 
@@ -398,7 +419,7 @@ function getActiveProfile(projectDir = process.cwd(), targetFileOrScope = null)
398
419
  }
399
420
 
400
421
  if (matchedProfileName) {
401
- const overrideProf = getProfile(matchedProfileName);
422
+ const overrideProf = getProfile(matchedProfileName, projectDir);
402
423
  if (overrideProf) {
403
424
  return {
404
425
  ...overrideProf,
@@ -424,9 +445,9 @@ function getActiveProfile(projectDir = process.cwd(), targetFileOrScope = null)
424
445
  * @returns {Object} Updated profile lock data
425
446
  */
426
447
  function applyProfile(profileName, projectDir = process.cwd(), options = {}) {
427
- const profile = getProfile(profileName);
448
+ const profile = getProfile(profileName, projectDir);
428
449
  if (!profile) {
429
- throw new Error(`Profile '${profileName}' not found. Available profiles: ${listProfiles().map(p => p.id).join(', ')}`);
450
+ throw new Error(`Profile '${profileName}' not found. Available profiles: ${listProfiles(projectDir).map(p => p.id).join(', ')}`);
430
451
  }
431
452
 
432
453
  validateProfile(profile);
@@ -563,9 +584,9 @@ function removeActiveProfile(projectDir = process.cwd(), options = {}) {
563
584
  * @returns {Object} Explanation structure
564
585
  */
565
586
  function explainProfile(profileName, projectDir = process.cwd()) {
566
- const profile = getProfile(profileName);
587
+ const profile = getProfile(profileName, projectDir);
567
588
  if (!profile) {
568
- throw new Error(`Profile '${profileName}' not found. Available profiles: ${listProfiles().map(p => p.id).join(', ')}`);
589
+ throw new Error(`Profile '${profileName}' not found. Available profiles: ${listProfiles(projectDir).map(p => p.id).join(', ')}`);
569
590
  }
570
591
 
571
592
  return {
package/README.md CHANGED
@@ -40,7 +40,16 @@ You do not need to clone anything manually. Just open your terminal in the root
40
40
  npx contextos-agents init
41
41
  ```
42
42
 
43
- The script will automatically detect your project tech stack, create the `.agents` folder, configure a neutral bootstrap profile, and compile it for your AI agent.
43
+ By default, ContextOS sets up a focused **Lean Core** (7 essential skills: `engineering-workflow`, `ponytail-mindset`, `gemini-precision`, `security`, `context-os`, `context-manager`, and `gstack-roles`). This ensures your AI agent context remains lightweight and focused, preventing context degradation.
44
+
45
+ Want more skills right away? Install pre-packaged presets or the entire catalog:
46
+
47
+ ```bash
48
+ npx contextos-agents init --preset frontend # React, Next.js, TypeScript, UI/UX, a11y
49
+ npx contextos-agents init --preset backend # System design, API design, Node.js, databases
50
+ npx contextos-agents init --preset devops # Docker, CI/CD, Terraform
51
+ npx contextos-agents init --all # Install all 36 catalog skills at once
52
+ ```
44
53
 
45
54
  ### Options
46
55
 
@@ -48,6 +57,8 @@ The script will automatically detect your project tech stack, create the `.agent
48
57
  npx contextos-agents --help # Show all options
49
58
  npx contextos-agents --version # Show version
50
59
  npx contextos-agents --minimal # Install only the core bootstrap skills
60
+ npx contextos-agents --all # Install all 36 catalog domain skills during init
61
+ npx contextos-agents --preset <name> # Install stack preset: frontend, backend, devops, full
51
62
  npx contextos-agents --profile init # Install with specific profile
52
63
  npx contextos-agents --auto # Auto-detect tech stack and apply recommended profile
53
64
  npx contextos-agents --dry-run # Preview what will be installed
@@ -110,6 +121,30 @@ contextos resolve "Build an accessible modal component with React and Tailwind"
110
121
  contextos resolve "security review" --files apps/web/app/login/page.tsx --explain
111
122
  ```
112
123
 
124
+ ### Skill & Catalog Management (`contextos skill`)
125
+
126
+ Discover, install, and customize skills:
127
+
128
+ ```bash
129
+ # Explore all available catalog skills (36 domain skills):
130
+ contextos skill list --available
131
+
132
+ # Install a specific skill from the catalog (with typo suggestions):
133
+ contextos skill add fastapi
134
+
135
+ # Install all 36 catalog skills at once:
136
+ contextos skill add --all
137
+
138
+ # Fork a built-in skill into your project for team customizations:
139
+ contextos skill override gemini-precision
140
+
141
+ # Diff your local customizations against upstream updates:
142
+ contextos skill diff gemini-precision
143
+
144
+ # Eject a skill to decouple it from upstream updates:
145
+ contextos skill eject gemini-precision
146
+ ```
147
+
113
148
  ### Diagnostic Health Check (`contextos doctor`)
114
149
 
115
150
  Run a comprehensive pre-flight verification across your repository to ensure valid skills, profile alignment, and compiler synchronization:
@@ -172,9 +207,9 @@ jobs:
172
207
  runs-on: ubuntu-latest
173
208
  steps:
174
209
  - uses: actions/checkout@v4
175
- - uses: kok-o/contextos-agents/.github/actions/contextos-gate@v2.0.0
210
+ - uses: kok-o/contextos-agents/.github/actions/contextos-gate@v2.2.0
176
211
  with:
177
- version: '2.0.0' # Pinned version of contextos-agents runner
212
+ version: '2.2.0' # Pinned version of contextos-agents runner
178
213
  adapters: 'all' # Adapters to verify (or specific: 'cursor', 'claude')
179
214
  working-directory: '.' # Project root directory
180
215
  ```