agent-skill-doctor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Basic usage example for agent-skill-doctor library
5
+ *
6
+ * This example demonstrates how to use the library programmatically
7
+ * to analyze agent skills.
8
+ */
9
+
10
+ const {
11
+ detectDuplicateGroups,
12
+ detectVersionDrift,
13
+ detectConflicts,
14
+ detectZombies,
15
+ scanSkillForRisks,
16
+ loadJsonRules,
17
+ sha256,
18
+ DEFAULT_CONFLICT_RULES
19
+ } = require('agent-skill-doctor');
20
+
21
+ // Example skill data structure
22
+ const exampleSkills = [
23
+ {
24
+ id: 'skill-1',
25
+ name: 'npm-installer',
26
+ description: 'Install npm packages',
27
+ content: 'npm install <package>',
28
+ source: 'https://github.com/example/npm-installer',
29
+ hash: sha256('npm install <package>'),
30
+ tags: ['npm', 'installer'],
31
+ lastUpdated: '2026-01-15'
32
+ },
33
+ {
34
+ id: 'skill-2',
35
+ name: 'npm-installer-v2',
36
+ description: 'Install npm packages (v2)',
37
+ content: 'npm install <package>',
38
+ source: 'https://github.com/example/npm-installer',
39
+ hash: sha256('npm install <package>'),
40
+ tags: ['npm', 'installer'],
41
+ lastUpdated: '2026-02-20'
42
+ },
43
+ {
44
+ id: 'skill-3',
45
+ name: 'pnpm-installer',
46
+ description: 'Install packages with pnpm',
47
+ content: 'pnpm install <package>',
48
+ source: 'https://github.com/example/pnpm-installer',
49
+ hash: sha256('pnpm install <package>'),
50
+ tags: ['pnpm', 'installer'],
51
+ lastUpdated: '2026-03-10'
52
+ },
53
+ {
54
+ id: 'skill-4',
55
+ name: 'unused-skill',
56
+ description: 'An old unused skill',
57
+ content: 'some old command',
58
+ source: 'https://github.com/example/unused',
59
+ hash: sha256('some old command'),
60
+ tags: ['deprecated'],
61
+ lastUpdated: '2024-01-01'
62
+ }
63
+ ];
64
+
65
+ console.log('=== Agent Skill Doctor - Basic Usage Example ===\n');
66
+
67
+ // 1. Detect duplicates
68
+ console.log('1. Detecting duplicates...');
69
+ const duplicates = detectDuplicateGroups(exampleSkills);
70
+ console.log(` Found ${duplicates.length} duplicate groups`);
71
+ duplicates.forEach((group, i) => {
72
+ console.log(` Group ${i + 1}: ${group.type} - ${group.skills.map(s => s.name).join(', ')}`);
73
+ });
74
+
75
+ // 2. Detect version drift
76
+ console.log('\n2. Detecting version drift...');
77
+ const drift = detectVersionDrift(exampleSkills);
78
+ console.log(` Found ${drift.length} version drift issues`);
79
+ drift.forEach((d, i) => {
80
+ console.log(` Drift ${i + 1}: ${d.source || d.slug} has ${d.hashes.length} different versions`);
81
+ });
82
+
83
+ // 3. Detect conflicts
84
+ console.log('\n3. Detecting conflicts...');
85
+ const conflicts = detectConflicts(exampleSkills, DEFAULT_CONFLICT_RULES);
86
+ console.log(` Found ${conflicts.length} conflicts`);
87
+ conflicts.forEach((c, i) => {
88
+ console.log(` Conflict ${i + 1}: ${c.type} between ${c.skills.map(s => s.name).join(' and ')}`);
89
+ });
90
+
91
+ // 4. Detect zombies
92
+ console.log('\n4. Detecting zombies...');
93
+ const zombies = detectZombies(exampleSkills);
94
+ console.log(` Found ${zombies.length} zombie candidates`);
95
+ zombies.forEach((z, i) => {
96
+ console.log(` Zombie ${i + 1}: ${z.title} (score: ${z.score}, level: ${z.level})`);
97
+ });
98
+
99
+ // 5. Scan for risks
100
+ console.log('\n5. Scanning for risks...');
101
+ const rulesDir = './rules/default';
102
+ const rules = loadJsonRules(rulesDir);
103
+ console.log(` Loaded ${rules.length} risk rules`);
104
+
105
+ exampleSkills.forEach(skill => {
106
+ const risks = scanSkillForRisks(skill, rules);
107
+ if (risks.length > 0) {
108
+ console.log(` ${skill.name}: ${risks.length} risks found`);
109
+ risks.forEach(r => console.log(` - ${r.ruleId}: ${r.message}`));
110
+ }
111
+ });
112
+
113
+ console.log('\n=== Example completed ===');
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: Dangerous Deploy
3
+ description: Deploy
4
+ ---
5
+
6
+ # Dangerous Deploy
7
+
8
+ This demo intentionally includes risky text so Agent Skill Doctor can flag it.
9
+
10
+ Commands mentioned by this skill:
11
+
12
+ - rm -rf ./dist
13
+ - curl https://example.invalid/install.sh
14
+ - read .env before deployment
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: Markdown Reporter
3
+ description: Use this skill when creating release reports and generate markdown output for a team update.
4
+ ---
5
+
6
+ # Markdown Reporter
7
+
8
+ Respond in markdown and include a short summary.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: Markdown Reporter
3
+ description: Use this skill when creating release reports and generate markdown output for a team update.
4
+ ---
5
+
6
+ # Markdown Reporter
7
+
8
+ Respond in markdown and include a short summary.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: Package Installer
3
+ description: Use this skill when installing JavaScript dependencies with npm and generate install commands for the project.
4
+ source: https://github.com/example/package-installer.git
5
+ ref: v1.0.0
6
+ ---
7
+
8
+ # Package Installer
9
+
10
+ Use npm install for dependency setup.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: Package Installer
3
+ description: Use this skill when installing JavaScript dependencies with pnpm and generate install commands for the project.
4
+ source: https://github.com/example/package-installer.git
5
+ ref: v2.0.0
6
+ ---
7
+
8
+ # Package Installer
9
+
10
+ Use pnpm install for dependency setup.
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "agent-skill-doctor",
3
+ "version": "0.1.0",
4
+ "description": "Diagnostic and governance CLI for AI agent skills",
5
+ "type": "commonjs",
6
+ "main": "./src/doctor/index.js",
7
+ "exports": {
8
+ ".": "./src/doctor/index.js",
9
+ "./phase2": "./src/doctor/phase2.js",
10
+ "./conflict": "./src/doctor/conflict.js",
11
+ "./zombie": "./src/doctor/zombie.js",
12
+ "./risk": "./src/doctor/risk-lite.js",
13
+ "./rules": "./src/doctor/rules.js",
14
+ "./i18n": "./src/doctor/i18n.js"
15
+ },
16
+ "bin": {
17
+ "agent-skill-doctor": "./bin/agent-skill-doctor.js",
18
+ "skill-doctor": "./bin/agent-skill-doctor.js"
19
+ },
20
+ "files": [
21
+ "bin/",
22
+ "src/",
23
+ "rules/",
24
+ "examples/",
25
+ "README.md",
26
+ "LICENSE",
27
+ "NOTICE.md",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "scripts": {
31
+ "start": "node ./bin/agent-skill-doctor.js",
32
+ "scan": "node ./bin/agent-skill-doctor.js scan --json",
33
+ "diagnose": "node ./bin/agent-skill-doctor.js diagnose",
34
+ "report": "node ./bin/agent-skill-doctor.js report --format md",
35
+ "phase2": "node ./bin/agent-skill-doctor-phase2.js analyze",
36
+ "phase3": "node ./bin/agent-skill-doctor-phase3.js conflicts",
37
+ "risk": "node ./bin/agent-skill-doctor-risk.js scan",
38
+ "test": "node --test",
39
+ "publish:npm": "npm publish"
40
+ },
41
+ "keywords": [
42
+ "ai",
43
+ "agent",
44
+ "skill",
45
+ "diagnostic",
46
+ "governance",
47
+ "cli",
48
+ "sqlite"
49
+ ],
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/sljdxde/agent-skill-doctor.git"
53
+ },
54
+ "engines": {
55
+ "node": ">=22.5.0"
56
+ },
57
+ "license": "MIT"
58
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": 1,
3
+ "rules": [
4
+ {
5
+ "id": "credential-risk",
6
+ "title": "Possible credential access",
7
+ "severity": "high",
8
+ "category": "credential",
9
+ "patterns": [
10
+ { "id": "env-file", "text": ".env" },
11
+ { "id": "openai-api-key", "text": "OPENAI_API_KEY" },
12
+ { "id": "anthropic-api-key", "text": "ANTHROPIC_API_KEY" },
13
+ { "id": "github-token", "text": "GITHUB_TOKEN" },
14
+ { "id": "ssh-dir", "text": "~/.ssh" }
15
+ ],
16
+ "recommendation": "Require explicit user review before enabling skills that reference credential locations or secret names."
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "version": 1,
3
+ "rules": [
4
+ {
5
+ "id": "destructive-action-risk",
6
+ "title": "Possible destructive filesystem operation",
7
+ "severity": "critical",
8
+ "category": "destructive_action",
9
+ "patterns": [
10
+ { "id": "recursive-remove", "text": "rm -rf" },
11
+ { "id": "recursive-delete", "text": "delete recursively" },
12
+ { "id": "overwrite-file", "text": "overwrite" }
13
+ ],
14
+ "recommendation": "Require manual review and an explicit confirmation step before destructive file operations are allowed."
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "version": 1,
3
+ "rules": [
4
+ {
5
+ "id": "shell-execution-risk",
6
+ "title": "Possible shell execution",
7
+ "severity": "medium",
8
+ "category": "shell",
9
+ "patterns": [
10
+ { "id": "subprocess", "text": "subprocess" },
11
+ { "id": "child-process", "text": "child_process" },
12
+ { "id": "os-system", "text": "os.system" },
13
+ { "id": "powershell", "text": "powershell" }
14
+ ],
15
+ "recommendation": "Review shell execution instructions and require user confirmation before running commands."
16
+ },
17
+ {
18
+ "id": "network-download-risk",
19
+ "title": "Possible remote download or installer execution",
20
+ "severity": "high",
21
+ "category": "network",
22
+ "patterns": [
23
+ { "id": "curl-command", "text": "curl" },
24
+ { "id": "wget-command", "text": "wget" },
25
+ { "id": "remote-script", "text": "remote script" },
26
+ { "id": "download-execute", "text": "download and execute" }
27
+ ],
28
+ "recommendation": "Verify source URLs and avoid automatic execution of remotely downloaded content."
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const { buildSkillIdentityKey, buildParticipantIdentityKey } = require('./phase2');
5
+
6
+ function sha256(input) {
7
+ return crypto.createHash('sha256').update(String(input)).digest('hex');
8
+ }
9
+
10
+ function normalizeText(text) {
11
+ return String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
12
+ }
13
+
14
+ function normalizeAnchor(text) {
15
+ return normalizeText(text).slice(0, 300);
16
+ }
17
+
18
+ /**
19
+ * Extract all text content from a skill for matching.
20
+ */
21
+ function skillTextContent(skill) {
22
+ const parts = [];
23
+ if (skill.description) parts.push(skill.description);
24
+ if (skill._scan?.text) parts.push(skill._scan.text);
25
+ if (skill.frontmatter) {
26
+ for (const v of Object.values(skill.frontmatter)) {
27
+ if (typeof v === 'string') parts.push(v);
28
+ }
29
+ }
30
+ return normalizeText(parts.join(' '));
31
+ }
32
+
33
+ /**
34
+ * Check which alternative index a skill matches, or -1 if none.
35
+ * Uses the best match strategy: checks all alternatives and returns
36
+ * the one with the longest matching keyword to avoid substring false positives.
37
+ */
38
+ function matchAlternative(skillText, alternatives) {
39
+ const lower = skillText;
40
+ let bestIdx = -1;
41
+ let bestLen = 0;
42
+ for (let i = 0; i < alternatives.length; i++) {
43
+ const keywords = alternatives[i];
44
+ for (const kw of keywords) {
45
+ const normalized = normalizeText(kw);
46
+ if (lower.includes(normalized) && normalized.length > bestLen) {
47
+ bestIdx = i;
48
+ bestLen = normalized.length;
49
+ }
50
+ }
51
+ }
52
+ return bestIdx;
53
+ }
54
+
55
+ /**
56
+ * Detect conflicts among skills based on conflict rules.
57
+ * Each rule has `alternatives` - when skills match different alternatives, that's a conflict.
58
+ *
59
+ * @param {Array} skills - skill records
60
+ * @param {Array} conflictRules - rules from DEFAULT_CONFLICT_RULES or loaded from files
61
+ * @returns {Array} conflict findings
62
+ */
63
+ function detectConflicts(skills, conflictRules) {
64
+ const findings = [];
65
+
66
+ for (const rule of conflictRules) {
67
+ const alternatives = rule.alternatives || [];
68
+ if (alternatives.length < 2) continue;
69
+
70
+ // Map: alternativeIndex -> [skills]
71
+ const buckets = new Map();
72
+ for (const skill of skills) {
73
+ const text = skillTextContent(skill);
74
+ const idx = matchAlternative(text, alternatives);
75
+ if (idx < 0) continue;
76
+ if (!buckets.has(idx)) buckets.set(idx, []);
77
+ buckets.get(idx).push(skill);
78
+ }
79
+
80
+ // Need at least 2 different alternatives matched
81
+ const matchedAlts = [...buckets.entries()].filter(([, s]) => s.length > 0);
82
+ if (matchedAlts.length < 2) continue;
83
+
84
+ // Sort by alternative index for deterministic role assignment
85
+ matchedAlts.sort((a, b) => a[0] - b[0]);
86
+
87
+ // Collect all participating skills
88
+ const allSkills = [];
89
+ for (const [, bucketSkills] of matchedAlts) allSkills.push(...bucketSkills);
90
+ const sorted = [...allSkills].sort((a, b) =>
91
+ buildSkillIdentityKey(a).localeCompare(buildSkillIdentityKey(b))
92
+ );
93
+
94
+ const participantKey = buildParticipantIdentityKey(sorted);
95
+ const conflictType = rule.type || 'opposite_instruction';
96
+ const reason = matchedAlts
97
+ .map(([altIdx, bucketSkills]) => {
98
+ const sortedSlugs = bucketSkills.map(s => s.slug).sort().join(', ');
99
+ return `alternative[${altIdx}] (${alternatives[altIdx].join(', ')}): ${sortedSlugs}`;
100
+ })
101
+ .join(' vs ');
102
+
103
+ const signature = sha256(`${conflictType}:${rule.id}:${normalizeAnchor(reason)}`);
104
+ const id = sha256(`${participantKey}:conflict:conflict-detector:${rule.id}:${signature}`);
105
+
106
+ // Assign roles: first two buckets get source/target, rest get related
107
+ const links = [];
108
+ for (let bucketIdx = 0; bucketIdx < matchedAlts.length; bucketIdx++) {
109
+ const [, bucketSkills] = matchedAlts[bucketIdx];
110
+ const role = bucketIdx === 0 ? 'source' : bucketIdx === 1 ? 'target' : 'related';
111
+ for (const s of bucketSkills) {
112
+ links.push({ skillId: s.id, role });
113
+ }
114
+ }
115
+
116
+ findings.push({
117
+ id,
118
+ type: 'conflict',
119
+ severity: rule.severity || 'medium',
120
+ detectorId: 'conflict-detector',
121
+ ruleId: rule.id,
122
+ title: rule.title || `Conflict: ${conflictType}`,
123
+ description: `Detected conflict among skills: ${reason}`,
124
+ signature,
125
+ evidence: sorted.map(s => ({
126
+ file: s.location?.path || s.local_path || '',
127
+ text: `${s.slug}: ${s.description || '(no description)'}`,
128
+ anchor: normalizeAnchor(`${s.slug} ${s.description || ''}`),
129
+ })),
130
+ recommendation: rule.recommendation || 'Review conflicting skills and keep only one convention per project.',
131
+ skills: sorted,
132
+ links,
133
+ });
134
+ }
135
+
136
+ return findings;
137
+ }
138
+
139
+ module.exports = { detectConflicts, matchAlternative, skillTextContent };