@skillsmith/cli 0.3.0 → 0.3.2

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 (107) hide show
  1. package/README.md +158 -0
  2. package/assets/skillsmith-skill/SKILL.md +235 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/src/commands/author/index.d.ts +16 -0
  5. package/dist/src/commands/author/index.d.ts.map +1 -0
  6. package/dist/src/commands/author/index.js +18 -0
  7. package/dist/src/commands/author/index.js.map +1 -0
  8. package/dist/src/commands/author/init.d.ts +47 -0
  9. package/dist/src/commands/author/init.d.ts.map +1 -0
  10. package/dist/src/commands/author/init.js +346 -0
  11. package/dist/src/commands/author/init.js.map +1 -0
  12. package/dist/src/commands/author/mcp-init.d.ts +20 -0
  13. package/dist/src/commands/author/mcp-init.d.ts.map +1 -0
  14. package/dist/src/commands/author/mcp-init.js +183 -0
  15. package/dist/src/commands/author/mcp-init.js.map +1 -0
  16. package/dist/src/commands/author/subagent.d.ts +22 -0
  17. package/dist/src/commands/author/subagent.d.ts.map +1 -0
  18. package/dist/src/commands/author/subagent.js +166 -0
  19. package/dist/src/commands/author/subagent.js.map +1 -0
  20. package/dist/src/commands/author/transform.d.ts +22 -0
  21. package/dist/src/commands/author/transform.d.ts.map +1 -0
  22. package/dist/src/commands/author/transform.js +141 -0
  23. package/dist/src/commands/author/transform.js.map +1 -0
  24. package/dist/src/commands/author/utils.d.ts +27 -0
  25. package/dist/src/commands/author/utils.d.ts.map +1 -0
  26. package/dist/src/commands/author/utils.js +118 -0
  27. package/dist/src/commands/author/utils.js.map +1 -0
  28. package/dist/src/commands/index.d.ts +3 -1
  29. package/dist/src/commands/index.d.ts.map +1 -1
  30. package/dist/src/commands/index.js +6 -1
  31. package/dist/src/commands/index.js.map +1 -1
  32. package/dist/src/commands/install-skill.d.ts +13 -0
  33. package/dist/src/commands/install-skill.d.ts.map +1 -0
  34. package/dist/src/commands/install-skill.js +137 -0
  35. package/dist/src/commands/install-skill.js.map +1 -0
  36. package/dist/src/commands/manage.d.ts +4 -1
  37. package/dist/src/commands/manage.d.ts.map +1 -1
  38. package/dist/src/commands/manage.js +56 -10
  39. package/dist/src/commands/manage.js.map +1 -1
  40. package/dist/src/commands/merge.d.ts +17 -0
  41. package/dist/src/commands/merge.d.ts.map +1 -0
  42. package/dist/src/commands/merge.js +160 -0
  43. package/dist/src/commands/merge.js.map +1 -0
  44. package/dist/src/commands/recommend.d.ts +1 -4
  45. package/dist/src/commands/recommend.d.ts.map +1 -1
  46. package/dist/src/commands/recommend.helpers.d.ts +58 -0
  47. package/dist/src/commands/recommend.helpers.d.ts.map +1 -0
  48. package/dist/src/commands/recommend.helpers.js +428 -0
  49. package/dist/src/commands/recommend.helpers.js.map +1 -0
  50. package/dist/src/commands/recommend.js +50 -372
  51. package/dist/src/commands/recommend.js.map +1 -1
  52. package/dist/src/commands/recommend.types.d.ts +66 -0
  53. package/dist/src/commands/recommend.types.d.ts.map +1 -0
  54. package/dist/src/commands/recommend.types.js +14 -0
  55. package/dist/src/commands/recommend.types.js.map +1 -0
  56. package/dist/src/commands/search.d.ts.map +1 -1
  57. package/dist/src/commands/search.js +133 -18
  58. package/dist/src/commands/search.js.map +1 -1
  59. package/dist/src/commands/sync.d.ts.map +1 -1
  60. package/dist/src/commands/sync.js +6 -46
  61. package/dist/src/commands/sync.js.map +1 -1
  62. package/dist/src/config.d.ts +5 -0
  63. package/dist/src/config.d.ts.map +1 -1
  64. package/dist/src/config.js +7 -0
  65. package/dist/src/config.js.map +1 -1
  66. package/dist/src/import.d.ts +1 -0
  67. package/dist/src/import.d.ts.map +1 -1
  68. package/dist/src/import.js +20 -5
  69. package/dist/src/import.js.map +1 -1
  70. package/dist/src/index.d.ts +1 -0
  71. package/dist/src/index.d.ts.map +1 -1
  72. package/dist/src/index.js +11 -1
  73. package/dist/src/index.js.map +1 -1
  74. package/dist/src/utils/formatters.d.ts +39 -0
  75. package/dist/src/utils/formatters.d.ts.map +1 -0
  76. package/dist/src/utils/formatters.js +69 -0
  77. package/dist/src/utils/formatters.js.map +1 -0
  78. package/dist/src/utils/license.test.js +6 -1
  79. package/dist/src/utils/license.test.js.map +1 -1
  80. package/dist/src/utils/node-version.d.ts +41 -0
  81. package/dist/src/utils/node-version.d.ts.map +1 -0
  82. package/dist/src/utils/node-version.js +123 -0
  83. package/dist/src/utils/node-version.js.map +1 -0
  84. package/dist/tests/author.test.js +45 -45
  85. package/dist/tests/author.test.js.map +1 -1
  86. package/dist/tests/e2e/search.e2e.test.js +62 -6
  87. package/dist/tests/e2e/search.e2e.test.js.map +1 -1
  88. package/dist/tests/e2e/utils/hardcoded-detector.d.ts.map +1 -1
  89. package/dist/tests/e2e/utils/hardcoded-detector.js +44 -3
  90. package/dist/tests/e2e/utils/hardcoded-detector.js.map +1 -1
  91. package/dist/tests/install-skill.test.d.ts +8 -0
  92. package/dist/tests/install-skill.test.d.ts.map +1 -0
  93. package/dist/tests/install-skill.test.js +409 -0
  94. package/dist/tests/install-skill.test.js.map +1 -0
  95. package/dist/tests/manage.test.js +284 -8
  96. package/dist/tests/manage.test.js.map +1 -1
  97. package/dist/tests/node-version.test.d.ts +8 -0
  98. package/dist/tests/node-version.test.d.ts.map +1 -0
  99. package/dist/tests/node-version.test.js +200 -0
  100. package/dist/tests/node-version.test.js.map +1 -0
  101. package/dist/tests/recommend.test.js +94 -0
  102. package/dist/tests/recommend.test.js.map +1 -1
  103. package/package.json +3 -2
  104. package/dist/src/commands/author.d.ts +0 -90
  105. package/dist/src/commands/author.d.ts.map +0 -1
  106. package/dist/src/commands/author.js +0 -902
  107. package/dist/src/commands/author.js.map +0 -1
@@ -0,0 +1,160 @@
1
+ /**
2
+ * SMI-1455: CLI command for safe skill database merging
3
+ *
4
+ * Provides a user-friendly interface to merge skill databases using
5
+ * the merge tooling from @skillsmith/core (SMI-1448).
6
+ *
7
+ * Usage:
8
+ * skillsmith merge <source-db> [target-db]
9
+ * sklx merge <source-db> --strategy keep_newer --dry-run
10
+ */
11
+ import { Command } from 'commander';
12
+ import { resolve } from 'path';
13
+ import { existsSync } from 'fs';
14
+ import Database from 'better-sqlite3';
15
+ import { mergeSkillDatabases, checkSchemaCompatibility, } from '@skillsmith/core';
16
+ import { getDefaultDbPath } from '../config.js';
17
+ /**
18
+ * Format merge result for display
19
+ */
20
+ function formatMergeResult(result) {
21
+ const lines = [
22
+ '',
23
+ '╔══════════════════════════════════════════════════════════════╗',
24
+ '║ Merge Results ║',
25
+ '╠══════════════════════════════════════════════════════════════╣',
26
+ `║ Skills added: ${result.skillsAdded.toString().padStart(8)} ║`,
27
+ `║ Skills updated: ${result.skillsUpdated.toString().padStart(8)} ║`,
28
+ `║ Skills skipped: ${result.skillsSkipped.toString().padStart(8)} ║`,
29
+ `║ Conflicts: ${result.conflicts.length.toString().padStart(8)} ║`,
30
+ `║ Duration: ${(result.duration / 1000).toFixed(2).padStart(8)}s ║`,
31
+ '╚══════════════════════════════════════════════════════════════╝',
32
+ '',
33
+ ];
34
+ return lines.join('\n');
35
+ }
36
+ /**
37
+ * Create the merge command
38
+ */
39
+ export function createMergeCommand() {
40
+ const command = new Command('merge')
41
+ .description('Merge skills from one database into another')
42
+ .argument('<source>', 'Source database path to merge from')
43
+ .argument('[target]', 'Target database path (default: local skills.db)')
44
+ .option('-s, --strategy <strategy>', 'Merge strategy: keep_target, keep_source, keep_newer, merge_fields', 'keep_newer')
45
+ .option('-d, --dry-run', 'Preview changes without applying them', false)
46
+ .option('-v, --verbose', 'Show detailed conflict information', false)
47
+ .option('-q, --quiet', 'Only output errors', false)
48
+ .option('--force', 'Skip compatibility checks', false)
49
+ .action(async (sourcePath, targetPath, options) => {
50
+ const { strategy, dryRun, verbose, quiet, force } = options;
51
+ // Validate strategy
52
+ const validStrategies = [
53
+ 'keep_target',
54
+ 'keep_source',
55
+ 'keep_newer',
56
+ 'merge_fields',
57
+ ];
58
+ if (!validStrategies.includes(strategy)) {
59
+ console.error(`Invalid strategy: ${strategy}`);
60
+ console.error(`Valid strategies: ${validStrategies.join(', ')}`);
61
+ process.exit(1);
62
+ }
63
+ // Resolve paths
64
+ const resolvedSource = resolve(sourcePath);
65
+ const resolvedTarget = targetPath ? resolve(targetPath) : getDefaultDbPath();
66
+ // Check source exists
67
+ if (!existsSync(resolvedSource)) {
68
+ console.error(`Source database not found: ${resolvedSource}`);
69
+ process.exit(1);
70
+ }
71
+ // Check target exists (or will be created)
72
+ if (!existsSync(resolvedTarget)) {
73
+ console.error(`Target database not found: ${resolvedTarget}`);
74
+ console.error('Create a new database first with: skillsmith init');
75
+ process.exit(1);
76
+ }
77
+ if (!quiet) {
78
+ console.log('╔══════════════════════════════════════════════════════════════╗');
79
+ console.log('║ Skillsmith Database Merge ║');
80
+ console.log('╠══════════════════════════════════════════════════════════════╣');
81
+ console.log(`║ Source: ${resolvedSource.slice(-48).padEnd(48)} ║`);
82
+ console.log(`║ Target: ${resolvedTarget.slice(-48).padEnd(48)} ║`);
83
+ console.log(`║ Strategy: ${strategy.padEnd(48)} ║`);
84
+ console.log(`║ Dry Run: ${(dryRun ? 'Yes' : 'No').padEnd(48)} ║`);
85
+ console.log('╚══════════════════════════════════════════════════════════════╝');
86
+ console.log('');
87
+ }
88
+ // Open databases
89
+ let sourceDb = null;
90
+ let targetDb = null;
91
+ try {
92
+ sourceDb = new Database(resolvedSource, { readonly: true });
93
+ targetDb = new Database(resolvedTarget);
94
+ // Check schema compatibility
95
+ if (!force) {
96
+ const sourceCompat = checkSchemaCompatibility(sourceDb);
97
+ const targetCompat = checkSchemaCompatibility(targetDb);
98
+ if (!sourceCompat.isCompatible) {
99
+ console.error(`Source database: ${sourceCompat.message}`);
100
+ process.exit(1);
101
+ }
102
+ if (!targetCompat.isCompatible) {
103
+ console.error(`Target database: ${targetCompat.message}`);
104
+ process.exit(1);
105
+ }
106
+ if (!quiet && sourceCompat.action !== 'none') {
107
+ console.log(`Source: ${sourceCompat.message}`);
108
+ }
109
+ if (!quiet && targetCompat.action !== 'none') {
110
+ console.log(`Target: ${targetCompat.message}`);
111
+ }
112
+ }
113
+ // Configure merge options
114
+ const mergeOptions = {
115
+ strategy: strategy,
116
+ dryRun,
117
+ skipInvalid: true,
118
+ ...(verbose && {
119
+ onConflict: (conflict) => {
120
+ console.log(` Conflict: ${conflict.skillId} (${conflict.reason})`);
121
+ return strategy;
122
+ },
123
+ }),
124
+ };
125
+ // Perform merge
126
+ if (!quiet) {
127
+ console.log('Merging databases...');
128
+ }
129
+ const result = mergeSkillDatabases(targetDb, sourceDb, mergeOptions);
130
+ // Display results
131
+ if (!quiet) {
132
+ console.log(formatMergeResult(result));
133
+ if (dryRun) {
134
+ console.log('⚠️ DRY RUN: No changes were made to the target database.');
135
+ console.log(' Remove --dry-run to apply these changes.');
136
+ }
137
+ else {
138
+ console.log('✅ Merge complete!');
139
+ }
140
+ }
141
+ // Exit with error if there were issues
142
+ if (result.skillsAdded === 0 && result.skillsUpdated === 0) {
143
+ if (!quiet) {
144
+ console.log('\nNo new skills to merge.');
145
+ }
146
+ }
147
+ }
148
+ catch (error) {
149
+ console.error('Merge failed:', error instanceof Error ? error.message : error);
150
+ process.exit(1);
151
+ }
152
+ finally {
153
+ sourceDb?.close();
154
+ targetDb?.close();
155
+ }
156
+ });
157
+ return command;
158
+ }
159
+ export default createMergeCommand;
160
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../../../src/commands/merge.ts"],"names":[],"mappings":"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"}
@@ -3,12 +3,9 @@
3
3
  *
4
4
  * Analyzes a codebase and recommends relevant skills based on detected
5
5
  * frameworks, dependencies, and patterns.
6
- *
7
- * References:
8
- * - SMI-1283 (analyze command pattern)
9
- * - packages/mcp-server/src/tools/recommend.ts (recommendation logic)
10
6
  */
11
7
  import { Command } from 'commander';
8
+ export type { SkillRecommendation, RecommendResponse, InstalledSkill } from './recommend.types.js';
12
9
  /**
13
10
  * Create recommend command
14
11
  */
@@ -1 +1 @@
1
- {"version":3,"file":"recommend.d.ts","sourceRoot":"","sources":["../../../src/commands/recommend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA0lBnC;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CA+BhD;AAED,eAAe,sBAAsB,CAAA"}
1
+ {"version":3,"file":"recommend.d.ts","sourceRoot":"","sources":["../../../src/commands/recommend.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAOnC,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAsKlG;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAkDhD;AAED,eAAe,sBAAsB,CAAA"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SMI-1299: CLI Recommend Command Helpers
3
+ * @module @skillsmith/cli/commands/recommend.helpers
4
+ */
5
+ import type { TrustTier, CodebaseContext, SkillRole } from '@skillsmith/core';
6
+ import type { SkillRecommendation, RecommendResponse, InstalledSkill } from './recommend.types.js';
7
+ /**
8
+ * Validate and normalize trust tier from API response (SMI-1354)
9
+ */
10
+ export declare function validateTrustTier(tier: unknown): TrustTier;
11
+ /**
12
+ * Check if error is a network-related error (SMI-1355)
13
+ */
14
+ export declare function isNetworkError(error: unknown): boolean;
15
+ /**
16
+ * Get trust badge for display (SMI-1357)
17
+ */
18
+ export declare function getTrustBadge(tier: TrustTier): string;
19
+ /**
20
+ * Format recommendations for terminal display
21
+ */
22
+ export declare function formatRecommendations(response: RecommendResponse, context: CodebaseContext | null): string;
23
+ /**
24
+ * Format recommendations as JSON
25
+ */
26
+ export declare function formatAsJson(response: RecommendResponse, context: CodebaseContext | null): string;
27
+ /**
28
+ * Format offline analysis results (SMI-1355)
29
+ */
30
+ export declare function formatOfflineResults(context: CodebaseContext, json: boolean): string;
31
+ /**
32
+ * Build stack from codebase analysis
33
+ */
34
+ export declare function buildStackFromAnalysis(context: CodebaseContext): string[];
35
+ /**
36
+ * Read installed skills from ~/.claude/skills/ directory (SMI-1358)
37
+ */
38
+ export declare function getInstalledSkills(): InstalledSkill[];
39
+ /**
40
+ * SMI-1631: Infer skill roles from tags when not explicitly set
41
+ */
42
+ export declare function inferRolesFromTags(tags: string[]): SkillRole[];
43
+ /**
44
+ * Normalize a skill name for comparison (SMI-1358)
45
+ */
46
+ export declare function normalizeSkillName(name: string): string;
47
+ /**
48
+ * Check if two skills overlap in functionality (SMI-1358)
49
+ */
50
+ export declare function skillsOverlap(installed: InstalledSkill, recommended: SkillRecommendation): boolean;
51
+ /**
52
+ * Filter recommendations to remove overlaps with installed skills (SMI-1358)
53
+ */
54
+ export declare function filterOverlappingSkills(recommendations: SkillRecommendation[], installedSkills: InstalledSkill[]): {
55
+ filtered: SkillRecommendation[];
56
+ overlapCount: number;
57
+ };
58
+ //# sourceMappingURL=recommend.helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommend.helpers.d.ts","sourceRoot":"","sources":["../../../src/commands/recommend.helpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EACf,SAAS,EAGV,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAOlG;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAK1D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CActD;AAMD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAYrD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,eAAe,GAAG,IAAI,GAC9B,MAAM,CAsFR;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,MAAM,CA+BjG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAyDpF;AAMD;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,EAAE,CAczE;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,cAAc,EAAE,CA0DrD;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAqE9D;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASvD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,cAAc,EACzB,WAAW,EAAE,mBAAmB,GAC/B,OAAO,CAqBT;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,mBAAmB,EAAE,EACtC,eAAe,EAAE,cAAc,EAAE,GAChC;IAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAkB3D"}
@@ -0,0 +1,428 @@
1
+ /**
2
+ * SMI-1299: CLI Recommend Command Helpers
3
+ * @module @skillsmith/cli/commands/recommend.helpers
4
+ */
5
+ import chalk from 'chalk';
6
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { VALID_TRUST_TIERS } from './recommend.types.js';
10
+ // ============================================================================
11
+ // Validation Helpers
12
+ // ============================================================================
13
+ /**
14
+ * Validate and normalize trust tier from API response (SMI-1354)
15
+ */
16
+ export function validateTrustTier(tier) {
17
+ if (typeof tier === 'string' && VALID_TRUST_TIERS.includes(tier)) {
18
+ return tier;
19
+ }
20
+ return 'unknown';
21
+ }
22
+ /**
23
+ * Check if error is a network-related error (SMI-1355)
24
+ */
25
+ export function isNetworkError(error) {
26
+ if (error instanceof Error) {
27
+ const message = error.message.toLowerCase();
28
+ return (message.includes('fetch failed') ||
29
+ message.includes('network') ||
30
+ message.includes('enotfound') ||
31
+ message.includes('econnrefused') ||
32
+ message.includes('timeout') ||
33
+ message.includes('socket') ||
34
+ error.name === 'AbortError');
35
+ }
36
+ return false;
37
+ }
38
+ // ============================================================================
39
+ // Display Helpers
40
+ // ============================================================================
41
+ /**
42
+ * Get trust badge for display (SMI-1357)
43
+ */
44
+ export function getTrustBadge(tier) {
45
+ switch (tier) {
46
+ case 'verified':
47
+ return chalk.green('[VERIFIED]');
48
+ case 'community':
49
+ return chalk.blue('[COMMUNITY]');
50
+ case 'experimental':
51
+ return chalk.yellow('[EXPERIMENTAL]');
52
+ case 'unknown':
53
+ default:
54
+ return chalk.gray('[UNKNOWN]');
55
+ }
56
+ }
57
+ /**
58
+ * Format recommendations for terminal display
59
+ */
60
+ export function formatRecommendations(response, context) {
61
+ const lines = [];
62
+ lines.push('');
63
+ lines.push(chalk.bold.blue('=== Skill Recommendations ==='));
64
+ lines.push('');
65
+ if (context) {
66
+ if (context.frameworks.length > 0) {
67
+ const frameworks = context.frameworks
68
+ .slice(0, 3)
69
+ .map((f) => f.name)
70
+ .join(', ');
71
+ lines.push(chalk.dim(`Detected: ${frameworks}`));
72
+ lines.push('');
73
+ }
74
+ }
75
+ if (response.recommendations.length === 0) {
76
+ lines.push(chalk.yellow('No recommendations found.'));
77
+ lines.push('');
78
+ lines.push('Suggestions:');
79
+ lines.push(' - Ensure the project has a package.json');
80
+ lines.push(' - Try a project with more dependencies');
81
+ lines.push(' - Use --context to provide additional context');
82
+ if (response.context.role_filter) {
83
+ lines.push(` - Try removing the --role filter (currently: ${response.context.role_filter})`);
84
+ }
85
+ }
86
+ else {
87
+ lines.push(`Found ${chalk.bold(response.recommendations.length)} recommendation(s):`);
88
+ lines.push('');
89
+ response.recommendations.forEach((rec, index) => {
90
+ const trustBadge = getTrustBadge(rec.trust_tier);
91
+ const qualityColor = rec.quality_score >= 80 ? chalk.green : rec.quality_score >= 50 ? chalk.yellow : chalk.red;
92
+ let relevanceDisplay;
93
+ if (rec.similarity_score < 0) {
94
+ relevanceDisplay = chalk.gray('N/A');
95
+ }
96
+ else {
97
+ const relevanceColor = rec.similarity_score >= 0.7
98
+ ? chalk.green
99
+ : rec.similarity_score >= 0.4
100
+ ? chalk.yellow
101
+ : chalk.gray;
102
+ relevanceDisplay = relevanceColor(`${Math.round(rec.similarity_score * 100)}%`);
103
+ }
104
+ const rolesDisplay = rec.roles?.length ? chalk.cyan(` [${rec.roles.join(', ')}]`) : '';
105
+ lines.push(`${chalk.bold(`${index + 1}.`)} ${chalk.bold(rec.name)} ${trustBadge}${rolesDisplay}`);
106
+ lines.push(` Score: ${qualityColor(`${rec.quality_score}/100`)} | Relevance: ${relevanceDisplay}`);
107
+ lines.push(` ${chalk.dim(rec.reason)}`);
108
+ lines.push(` ${chalk.dim(`ID: ${rec.skill_id}`)}`);
109
+ lines.push('');
110
+ });
111
+ }
112
+ lines.push(chalk.dim('---'));
113
+ lines.push(chalk.dim(`Candidates considered: ${response.candidates_considered}`));
114
+ if (response.overlap_filtered > 0) {
115
+ lines.push(chalk.dim(`Filtered for overlap: ${response.overlap_filtered}`));
116
+ }
117
+ if (response.role_filtered > 0) {
118
+ lines.push(chalk.dim(`Filtered for role: ${response.role_filtered}`));
119
+ }
120
+ if (response.context.role_filter) {
121
+ lines.push(chalk.dim(`Role filter: ${response.context.role_filter}`));
122
+ }
123
+ if (response.context.auto_detected) {
124
+ lines.push(chalk.dim(`Installed skills: ${response.context.installed_count} (auto-detected from ~/.claude/skills/)`));
125
+ }
126
+ else {
127
+ lines.push(chalk.dim(`Installed skills: ${response.context.installed_count}`));
128
+ }
129
+ lines.push(chalk.dim(`Completed in ${response.timing.totalMs}ms`));
130
+ return lines.join('\n');
131
+ }
132
+ /**
133
+ * Format recommendations as JSON
134
+ */
135
+ export function formatAsJson(response, context) {
136
+ const output = {
137
+ recommendations: response.recommendations,
138
+ analysis: context
139
+ ? {
140
+ frameworks: context.frameworks.slice(0, 10).map((f) => ({
141
+ name: f.name,
142
+ confidence: Math.round(f.confidence * 100),
143
+ })),
144
+ dependencies: context.dependencies.slice(0, 20).map((d) => ({
145
+ name: d.name,
146
+ is_dev: d.isDev,
147
+ })),
148
+ stats: {
149
+ total_files: context.stats.totalFiles,
150
+ total_lines: context.stats.totalLines,
151
+ },
152
+ }
153
+ : null,
154
+ meta: {
155
+ candidates_considered: response.candidates_considered,
156
+ overlap_filtered: response.overlap_filtered,
157
+ role_filtered: response.role_filtered,
158
+ role_filter: response.context.role_filter ?? null,
159
+ installed_count: response.context.installed_count,
160
+ auto_detected: response.context.auto_detected,
161
+ timing_ms: response.timing.totalMs,
162
+ },
163
+ };
164
+ return JSON.stringify(output, null, 2);
165
+ }
166
+ /**
167
+ * Format offline analysis results (SMI-1355)
168
+ */
169
+ export function formatOfflineResults(context, json) {
170
+ if (json) {
171
+ return JSON.stringify({
172
+ offline: true,
173
+ analysis: {
174
+ frameworks: context.frameworks.slice(0, 10).map((f) => ({
175
+ name: f.name,
176
+ confidence: Math.round(f.confidence * 100),
177
+ })),
178
+ dependencies: context.dependencies.slice(0, 20).map((d) => ({
179
+ name: d.name,
180
+ is_dev: d.isDev,
181
+ })),
182
+ stats: {
183
+ total_files: context.stats.totalFiles,
184
+ total_lines: context.stats.totalLines,
185
+ },
186
+ },
187
+ message: 'Unable to reach Skillsmith API. Showing analysis-only results.',
188
+ }, null, 2);
189
+ }
190
+ const lines = [];
191
+ lines.push('');
192
+ lines.push(chalk.yellow('⚠ Unable to reach Skillsmith API. Showing analysis-only results.'));
193
+ lines.push('');
194
+ lines.push(chalk.bold.blue('=== Codebase Analysis ==='));
195
+ lines.push('');
196
+ if (context.frameworks.length > 0) {
197
+ lines.push(chalk.bold('Detected Frameworks:'));
198
+ context.frameworks.slice(0, 5).forEach((f) => {
199
+ lines.push(` - ${f.name} (${Math.round(f.confidence * 100)}% confidence)`);
200
+ });
201
+ lines.push('');
202
+ }
203
+ if (context.dependencies.length > 0) {
204
+ const prodDeps = context.dependencies.filter((d) => !d.isDev).slice(0, 10);
205
+ if (prodDeps.length > 0) {
206
+ lines.push(chalk.bold('Key Dependencies:'));
207
+ prodDeps.forEach((d) => lines.push(` - ${d.name}`));
208
+ lines.push('');
209
+ }
210
+ }
211
+ lines.push(chalk.dim('---'));
212
+ lines.push(chalk.dim(`Files analyzed: ${context.stats.totalFiles}`));
213
+ lines.push(chalk.dim(`Total lines: ${context.stats.totalLines.toLocaleString()}`));
214
+ lines.push('');
215
+ lines.push(chalk.cyan('To get skill recommendations, ensure network connectivity and retry.'));
216
+ return lines.join('\n');
217
+ }
218
+ // ============================================================================
219
+ // Stack Building
220
+ // ============================================================================
221
+ /**
222
+ * Build stack from codebase analysis
223
+ */
224
+ export function buildStackFromAnalysis(context) {
225
+ const stack = [];
226
+ for (const fw of context.frameworks.slice(0, 5)) {
227
+ stack.push(fw.name.toLowerCase());
228
+ }
229
+ for (const dep of context.dependencies.slice(0, 10)) {
230
+ if (!dep.isDev) {
231
+ stack.push(dep.name.toLowerCase());
232
+ }
233
+ }
234
+ return [...new Set(stack)].slice(0, 10);
235
+ }
236
+ // ============================================================================
237
+ // Installed Skills Detection
238
+ // ============================================================================
239
+ /**
240
+ * Read installed skills from ~/.claude/skills/ directory (SMI-1358)
241
+ */
242
+ export function getInstalledSkills() {
243
+ const skillsDir = join(homedir(), '.claude', 'skills');
244
+ if (!existsSync(skillsDir)) {
245
+ return [];
246
+ }
247
+ const installedSkills = [];
248
+ try {
249
+ const entries = readdirSync(skillsDir);
250
+ for (const entry of entries) {
251
+ const skillPath = join(skillsDir, entry);
252
+ const stat = statSync(skillPath);
253
+ if (!stat.isDirectory())
254
+ continue;
255
+ const skill = {
256
+ name: entry.toLowerCase(),
257
+ directory: entry,
258
+ tags: [],
259
+ category: null,
260
+ };
261
+ const skillMdPath = join(skillPath, 'SKILL.md');
262
+ if (existsSync(skillMdPath)) {
263
+ try {
264
+ const content = readFileSync(skillMdPath, 'utf-8');
265
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
266
+ const frontmatter = frontmatterMatch?.[1];
267
+ if (frontmatter) {
268
+ const tagsMatch = frontmatter.match(/tags:\s*\[(.*?)\]/);
269
+ const tagsContent = tagsMatch?.[1];
270
+ if (tagsContent) {
271
+ skill.tags = tagsContent
272
+ .split(',')
273
+ .map((t) => t.trim().replace(/['"]/g, '').toLowerCase())
274
+ .filter(Boolean);
275
+ }
276
+ const categoryMatch = frontmatter.match(/category:\s*["']?([^"'\n]+)["']?/);
277
+ const categoryContent = categoryMatch?.[1];
278
+ if (categoryContent) {
279
+ skill.category = categoryContent.trim().toLowerCase();
280
+ }
281
+ }
282
+ }
283
+ catch {
284
+ // Ignore read errors
285
+ }
286
+ }
287
+ installedSkills.push(skill);
288
+ }
289
+ }
290
+ catch {
291
+ return [];
292
+ }
293
+ return installedSkills;
294
+ }
295
+ // ============================================================================
296
+ // Role Inference
297
+ // ============================================================================
298
+ /**
299
+ * SMI-1631: Infer skill roles from tags when not explicitly set
300
+ */
301
+ export function inferRolesFromTags(tags) {
302
+ const roleMapping = {
303
+ lint: 'code-quality',
304
+ linting: 'code-quality',
305
+ format: 'code-quality',
306
+ formatting: 'code-quality',
307
+ prettier: 'code-quality',
308
+ eslint: 'code-quality',
309
+ 'code-review': 'code-quality',
310
+ review: 'code-quality',
311
+ refactor: 'code-quality',
312
+ refactoring: 'code-quality',
313
+ 'code-style': 'code-quality',
314
+ test: 'testing',
315
+ testing: 'testing',
316
+ jest: 'testing',
317
+ vitest: 'testing',
318
+ mocha: 'testing',
319
+ playwright: 'testing',
320
+ cypress: 'testing',
321
+ e2e: 'testing',
322
+ unit: 'testing',
323
+ integration: 'testing',
324
+ tdd: 'testing',
325
+ docs: 'documentation',
326
+ documentation: 'documentation',
327
+ readme: 'documentation',
328
+ jsdoc: 'documentation',
329
+ typedoc: 'documentation',
330
+ changelog: 'documentation',
331
+ api: 'documentation',
332
+ git: 'workflow',
333
+ commit: 'workflow',
334
+ pr: 'workflow',
335
+ 'pull-request': 'workflow',
336
+ ci: 'workflow',
337
+ cd: 'workflow',
338
+ 'ci-cd': 'workflow',
339
+ deploy: 'workflow',
340
+ deployment: 'workflow',
341
+ automation: 'workflow',
342
+ workflow: 'workflow',
343
+ security: 'security',
344
+ audit: 'security',
345
+ vulnerability: 'security',
346
+ cve: 'security',
347
+ secrets: 'security',
348
+ authentication: 'security',
349
+ auth: 'security',
350
+ ai: 'development-partner',
351
+ assistant: 'development-partner',
352
+ helper: 'development-partner',
353
+ copilot: 'development-partner',
354
+ productivity: 'development-partner',
355
+ scaffold: 'development-partner',
356
+ generator: 'development-partner',
357
+ };
358
+ const inferredRoles = new Set();
359
+ for (const tag of tags) {
360
+ const normalizedTag = tag.toLowerCase().replace(/[-_]/g, '');
361
+ for (const [keyword, role] of Object.entries(roleMapping)) {
362
+ if (normalizedTag.includes(keyword.replace(/[-_]/g, ''))) {
363
+ inferredRoles.add(role);
364
+ }
365
+ }
366
+ }
367
+ return [...inferredRoles];
368
+ }
369
+ // ============================================================================
370
+ // Overlap Detection
371
+ // ============================================================================
372
+ /**
373
+ * Normalize a skill name for comparison (SMI-1358)
374
+ */
375
+ export function normalizeSkillName(name) {
376
+ return name
377
+ .toLowerCase()
378
+ .replace(/[-_]/g, '')
379
+ .replace(/^skill/, '')
380
+ .replace(/skill$/, '')
381
+ .replace(/^helper/, '')
382
+ .replace(/helper$/, '')
383
+ .trim();
384
+ }
385
+ /**
386
+ * Check if two skills overlap in functionality (SMI-1358)
387
+ */
388
+ export function skillsOverlap(installed, recommended) {
389
+ const installedName = normalizeSkillName(installed.name);
390
+ const recommendedName = normalizeSkillName(recommended.name);
391
+ const recommendedId = recommended.skill_id.toLowerCase();
392
+ if (installedName === recommendedName)
393
+ return true;
394
+ if (recommendedId.includes(installed.name))
395
+ return true;
396
+ if (installedName.includes(recommendedName) || recommendedName.includes(installedName)) {
397
+ if (installedName.length >= 4 && recommendedName.length >= 4)
398
+ return true;
399
+ }
400
+ if (installed.tags.length > 0) {
401
+ const recommendedNameParts = recommended.name.toLowerCase().split(/[-_\s]+/);
402
+ const hasTagOverlap = installed.tags.some((tag) => recommendedNameParts.includes(tag) || recommendedName.includes(tag));
403
+ if (hasTagOverlap)
404
+ return true;
405
+ }
406
+ return false;
407
+ }
408
+ /**
409
+ * Filter recommendations to remove overlaps with installed skills (SMI-1358)
410
+ */
411
+ export function filterOverlappingSkills(recommendations, installedSkills) {
412
+ if (installedSkills.length === 0) {
413
+ return { filtered: recommendations, overlapCount: 0 };
414
+ }
415
+ const filtered = [];
416
+ let overlapCount = 0;
417
+ for (const rec of recommendations) {
418
+ const hasOverlap = installedSkills.some((installed) => skillsOverlap(installed, rec));
419
+ if (hasOverlap) {
420
+ overlapCount++;
421
+ }
422
+ else {
423
+ filtered.push(rec);
424
+ }
425
+ }
426
+ return { filtered, overlapCount };
427
+ }
428
+ //# sourceMappingURL=recommend.helpers.js.map