@sagargupta1610/skillcheck 0.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.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ checkEvals,
4
+ crossSkillFindings,
5
+ initEvals,
6
+ lintSkillDir,
7
+ toSarif
8
+ } from "./chunk-MGSKVMAQ.js";
9
+
10
+ // src/cli.ts
11
+ import { appendFile, readFile, stat, writeFile } from "fs/promises";
12
+ import { join } from "path";
13
+ import { Command } from "commander";
14
+ import { glob } from "glob";
15
+ var VERSION = "0.2.0";
16
+ var program = new Command();
17
+ program.name("skillcheck").description("Conformance suite for Agent Skills (SKILL.md)").version(VERSION);
18
+ program.command("lint").description(
19
+ "Lint one or more skill directories against the Agent Skills spec"
20
+ ).argument(
21
+ "<paths...>",
22
+ "skill directories (or parents containing */SKILL.md)"
23
+ ).option("--strict", "deprecated alias for --profile strict (default)", false).option(
24
+ "--profile <profile>",
25
+ "strict (skills-ref parity) | lenient (client-guide)",
26
+ "strict"
27
+ ).option(
28
+ "--format <format>",
29
+ "output format: pretty | concise | json | github | sarif",
30
+ "pretty"
31
+ ).option(
32
+ "--fail-on <severity>",
33
+ "minimum severity that fails: error | warning | never",
34
+ "error"
35
+ ).option("--sarif <path>", "also write a SARIF 2.1.0 report to this path").option("--max-warnings <n>", "fail when warnings exceed this count").action(async (paths, opts) => {
36
+ const skillDirs = await expandSkillDirs(paths);
37
+ if (skillDirs.length === 0) {
38
+ console.error("no SKILL.md found under the given paths");
39
+ process.exit(2);
40
+ }
41
+ const profile = opts.profile === "lenient" ? "lenient" : "strict";
42
+ const config = await loadConfig();
43
+ const results = [];
44
+ for (const dir of skillDirs) {
45
+ const result = await lintSkillDir(dir, {
46
+ profile,
47
+ ...config?.rules ? { ruleOverrides: config.rules } : {}
48
+ });
49
+ result.findings.push(...await checkEvals(dir, result.skillName));
50
+ recount(result);
51
+ results.push(result);
52
+ }
53
+ crossSkillFindings(results);
54
+ if (opts.sarif) {
55
+ await writeFile(opts.sarif, toSarif(results, VERSION), "utf8");
56
+ }
57
+ report(results, opts.format);
58
+ await emitGithubOutputs(results);
59
+ const errors = results.reduce((s, r) => s + r.errorCount, 0);
60
+ const warnings = results.reduce((s, r) => s + r.warningCount, 0);
61
+ const maxWarnings = opts.maxWarnings === void 0 ? Number.POSITIVE_INFINITY : Number(opts.maxWarnings);
62
+ let failed = false;
63
+ if (opts.failOn === "never") failed = false;
64
+ else if (opts.failOn === "warning") failed = errors > 0 || warnings > 0;
65
+ else failed = errors > 0;
66
+ if (warnings > maxWarnings) failed = true;
67
+ process.exit(failed ? 1 : 0);
68
+ });
69
+ var evalCmd = program.command("eval").description("Trigger-test definitions for skills");
70
+ evalCmd.command("init").description(
71
+ "Scaffold evals/evals.json for a skill (skill-creator-compatible format)"
72
+ ).argument("<skill-dir>", "skill directory").action(async (dir) => {
73
+ try {
74
+ const path = await initEvals(dir);
75
+ console.log(`created ${path}`);
76
+ console.log(
77
+ "Fill in the REPLACE placeholders: 3 explicit / 3 implicit / 3 contextual / 4 negative triggers."
78
+ );
79
+ } catch (err) {
80
+ console.error(err instanceof Error ? err.message : err);
81
+ process.exit(2);
82
+ }
83
+ });
84
+ evalCmd.command("check").description("Validate evals/evals.json against the schema").argument("<skill-dir>", "skill directory").action(async (dir) => {
85
+ const result = await lintSkillDir(dir);
86
+ const findings = await checkEvals(dir, result.skillName);
87
+ if (findings.length === 0) {
88
+ console.log("evals.json ok (or absent)");
89
+ process.exit(0);
90
+ }
91
+ for (const f of findings) {
92
+ console.log(
93
+ `${f.severity === "error" ? "ERR " : "WARN"} [${f.code}] ${f.message}`
94
+ );
95
+ }
96
+ process.exit(findings.some((f) => f.severity === "error") ? 1 : 0);
97
+ });
98
+ async function loadConfig() {
99
+ try {
100
+ const raw = await readFile("skillcheck.config.json", "utf8");
101
+ return JSON.parse(raw);
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+ async function expandSkillDirs(paths) {
107
+ const dirs = /* @__PURE__ */ new Set();
108
+ for (const p of paths) {
109
+ if (await exists(join(p, "SKILL.md")) || await exists(join(p, "skill.md"))) {
110
+ dirs.add(p);
111
+ continue;
112
+ }
113
+ const matches = await glob("**/{SKILL,skill}.md", {
114
+ cwd: p,
115
+ ignore: ["**/node_modules/**", "**/.git/**"],
116
+ absolute: true
117
+ });
118
+ for (const m of matches) {
119
+ dirs.add(join(m, ".."));
120
+ }
121
+ }
122
+ return [...dirs].sort();
123
+ }
124
+ async function exists(path) {
125
+ try {
126
+ await stat(path);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ function recount(r) {
133
+ r.errorCount = r.findings.filter((f) => f.severity === "error").length;
134
+ r.warningCount = r.findings.filter((f) => f.severity === "warning").length;
135
+ r.infoCount = r.findings.filter((f) => f.severity === "info").length;
136
+ }
137
+ async function emitGithubOutputs(results) {
138
+ const errors = results.reduce((s, r) => s + r.errorCount, 0);
139
+ const warnings = results.reduce((s, r) => s + r.warningCount, 0);
140
+ const outputPath = process.env.GITHUB_OUTPUT;
141
+ if (outputPath) {
142
+ await appendFile(
143
+ outputPath,
144
+ `error-count=${errors}
145
+ warning-count=${warnings}
146
+ `,
147
+ "utf8"
148
+ );
149
+ }
150
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
151
+ if (summaryPath) {
152
+ const lines = [
153
+ "## skillcheck",
154
+ "",
155
+ `**${results.length}** skill(s) checked: **${errors}** error(s), **${warnings}** warning(s)`,
156
+ ""
157
+ ];
158
+ const withFindings = results.filter((r) => r.findings.length > 0);
159
+ if (withFindings.length > 0) {
160
+ lines.push(
161
+ "| Skill | Rule | Severity | Message |",
162
+ "| --- | --- | --- | --- |"
163
+ );
164
+ for (const r of withFindings) {
165
+ for (const f of r.findings.slice(0, 50)) {
166
+ lines.push(
167
+ `| ${r.skillName ?? r.skillDir} | ${f.code} | ${f.severity} | ${f.message.replace(/\|/g, "\\|")} |`
168
+ );
169
+ }
170
+ }
171
+ }
172
+ await appendFile(summaryPath, `${lines.join("\n")}
173
+ `, "utf8");
174
+ }
175
+ }
176
+ function report(results, format) {
177
+ if (format === "json") {
178
+ console.log(JSON.stringify(results, null, 2));
179
+ return;
180
+ }
181
+ if (format === "sarif") {
182
+ console.log(toSarif(results, VERSION));
183
+ return;
184
+ }
185
+ if (format === "github") {
186
+ for (const r of results) {
187
+ for (const f of r.findings) {
188
+ const level = f.severity === "error" ? "error" : f.severity === "warning" ? "warning" : "notice";
189
+ const file = join(r.skillDir, f.file);
190
+ console.log(
191
+ `::${level} file=${file},line=${f.line ?? 1},title=${f.code} ${f.alias}::${f.message}`
192
+ );
193
+ }
194
+ }
195
+ return;
196
+ }
197
+ if (format === "concise") {
198
+ for (const r of results) {
199
+ for (const f of r.findings) {
200
+ const glyph = f.severity === "error" ? "x" : f.severity === "warning" ? "!" : "-";
201
+ console.log(
202
+ `${glyph} ${r.skillDir}/${f.file}:${f.line ?? 1}: ${f.code} (${f.alias}): ${f.message}`
203
+ );
204
+ }
205
+ }
206
+ printTotals(results);
207
+ return;
208
+ }
209
+ for (const r of results) {
210
+ const label = r.skillName ?? "(unnamed)";
211
+ if (r.findings.length === 0) {
212
+ console.log(`ok ${label} ${r.skillDir}`);
213
+ continue;
214
+ }
215
+ console.log(`
216
+ ${label} ${r.skillDir}`);
217
+ for (const f of r.findings) {
218
+ const tag = f.severity === "error" ? "ERR " : f.severity === "warning" ? "WARN" : "INFO";
219
+ console.log(` ${tag} [${f.code} ${f.alias}] ${f.message}`);
220
+ if (f.suggestion) console.log(` fix: ${f.suggestion}`);
221
+ }
222
+ }
223
+ printTotals(results);
224
+ }
225
+ function printTotals(results) {
226
+ const errors = results.reduce((s, r) => s + r.errorCount, 0);
227
+ const warnings = results.reduce((s, r) => s + r.warningCount, 0);
228
+ const infos = results.reduce((s, r) => s + r.infoCount, 0);
229
+ console.log(
230
+ `
231
+ ${results.length} skill(s) checked: ${errors} error(s), ${warnings} warning(s), ${infos} info`
232
+ );
233
+ }
234
+ program.parseAsync().catch((err) => {
235
+ console.error(err instanceof Error ? err.message : err);
236
+ process.exit(2);
237
+ });
@@ -0,0 +1,139 @@
1
+ import { z } from 'zod';
2
+
3
+ /** Severity tiers: `error` = skills-ref 0.1.0 strict parity, `warning` =
4
+ * beyond-parity checks + client-guide lenient downgrades, `info` = advisory
5
+ * heuristics that never gate CI. */
6
+ type Severity = "error" | "warning" | "info";
7
+ /** Lint profile: `strict` = skills-ref parity (default), `lenient` =
8
+ * client-implementation-guide behavior (name-too-long and dir-mismatch
9
+ * downgrade to warnings; extension fields stay warnings). */
10
+ type Profile = "strict" | "lenient";
11
+ interface RuleMeta {
12
+ /** Stable code, e.g. "SC013". Never renumbered or recycled. */
13
+ code: string;
14
+ /** Kebab-case alias, interchangeable with the code everywhere. */
15
+ alias: string;
16
+ /** Default severity under the strict profile. */
17
+ severity: Severity;
18
+ /** Severity under the lenient profile (defaults to `severity`). */
19
+ lenientSeverity?: Severity;
20
+ /** Autofix classification; v0.2 tags but does not fix. */
21
+ fixable: "safe" | "unsafe" | null;
22
+ summary: string;
23
+ }
24
+ interface Finding {
25
+ code: string;
26
+ alias: string;
27
+ severity: Severity;
28
+ message: string;
29
+ /** Path of the file the finding is anchored to, relative to the skill dir. */
30
+ file: string;
31
+ line?: number;
32
+ /** Optional fix-it suggestion printed in pretty output. */
33
+ suggestion?: string;
34
+ }
35
+ interface LintResult {
36
+ skillDir: string;
37
+ skillName: string | null;
38
+ findings: Finding[];
39
+ errorCount: number;
40
+ warningCount: number;
41
+ infoCount: number;
42
+ }
43
+ interface LintOptions {
44
+ /** Lint profile. Default "strict" (skills-ref parity). */
45
+ profile?: Profile;
46
+ /** Per-rule severity overrides from config: code/alias -> off|warn|error. */
47
+ ruleOverrides?: Record<string, "off" | "warn" | "error" | "info">;
48
+ }
49
+ /** Parsed SKILL.md frontmatter. Unknown fields are preserved for rule checks. */
50
+ interface SkillFrontmatter {
51
+ name?: unknown;
52
+ description?: unknown;
53
+ license?: unknown;
54
+ compatibility?: unknown;
55
+ metadata?: unknown;
56
+ "allowed-tools"?: unknown;
57
+ [key: string]: unknown;
58
+ }
59
+ interface ParsedSkill {
60
+ frontmatter: SkillFrontmatter | null;
61
+ /** skills-ref-parity parse error message, if frontmatter is unusable. */
62
+ parseError: string | null;
63
+ /** Raw frontmatter text (for raw-text scans like SC021). */
64
+ rawFrontmatter: string;
65
+ /** Markdown body (everything after the closing frontmatter fence). */
66
+ body: string;
67
+ /** 1-based line number where the body starts. */
68
+ bodyStartLine: number;
69
+ }
70
+
71
+ /** Validate evals/evals.json against the schema + semantic rules.
72
+ * Returns findings (empty when the file is absent -- evals are optional). */
73
+ declare function checkEvals(skillDir: string, skillName: string | null): Promise<Finding[]>;
74
+ /** Scaffold evals/evals.json for a skill (skill-creator-compatible). */
75
+ declare function initEvals(skillDir: string, skillName?: string): Promise<string>;
76
+
77
+ declare const evalsFileSchema: z.ZodObject<{
78
+ skill_name: z.ZodString;
79
+ settings: z.ZodOptional<z.ZodObject<{
80
+ runs_per_prompt: z.ZodOptional<z.ZodNumber>;
81
+ trigger_threshold: z.ZodOptional<z.ZodNumber>;
82
+ }, z.core.$loose>>;
83
+ evals: z.ZodOptional<z.ZodArray<z.ZodObject<{
84
+ id: z.ZodNumber;
85
+ prompt: z.ZodString;
86
+ expected_output: z.ZodOptional<z.ZodString>;
87
+ files: z.ZodOptional<z.ZodArray<z.ZodString>>;
88
+ expectations: z.ZodArray<z.ZodString>;
89
+ }, z.core.$loose>>>;
90
+ triggers: z.ZodOptional<z.ZodArray<z.ZodObject<{
91
+ id: z.ZodString;
92
+ type: z.ZodEnum<{
93
+ explicit: "explicit";
94
+ implicit: "implicit";
95
+ contextual: "contextual";
96
+ negative: "negative";
97
+ }>;
98
+ prompt: z.ZodString;
99
+ should_trigger: z.ZodOptional<z.ZodBoolean>;
100
+ }, z.core.$loose>>>;
101
+ }, z.core.$loose>;
102
+
103
+ /** Typed registry of known client extension fields (beyond the agentskills.io
104
+ * spec). Drives SC301 (known extension, warning) and SC302 (invalid extension
105
+ * value). Sources: code.claude.com/docs skills pages, Cursor/Copilot/OpenCode
106
+ * docs, verified 2026-07-06. */
107
+ interface ExtensionField {
108
+ /** Expected JS type(s) after YAML parse. */
109
+ type: "string" | "boolean" | "string-or-array" | "object" | "enum";
110
+ /** For enum type: allowed values. */
111
+ values?: string[];
112
+ /** Runtimes that read this field. */
113
+ runtimes: string[];
114
+ deprecated?: string;
115
+ }
116
+ declare const KNOWN_EXTENSIONS: Record<string, ExtensionField>;
117
+ /** Spec-official frontmatter fields (ALLOWED_FIELDS in skills-ref). */
118
+ declare const SPEC_FIELDS: Set<string>;
119
+
120
+ /** Lint a single skill directory containing a SKILL.md (or skill.md). */
121
+ declare function lintSkillDir(dir: string, options?: LintOptions): Promise<LintResult>;
122
+ /** Cross-skill checks over a set of results (SC105 duplicate names). */
123
+ declare function crossSkillFindings(results: LintResult[]): void;
124
+
125
+ /** Split a SKILL.md source into frontmatter + body, replicating skills-ref
126
+ * parser.py semantics exactly (split-based, error strings verbatim). */
127
+ declare function parseSkillMd(source: string): ParsedSkill;
128
+
129
+ /** Rule registry: codes are stable, never renumbered or recycled.
130
+ * SC0xx frontmatter, SC1xx structure/references, SC2xx body, SC3xx extensions. */
131
+ declare const RULES: RuleMeta[];
132
+ declare function getRule(codeOrAlias: string): RuleMeta | undefined;
133
+
134
+ /** Emit SARIF 2.1.0 (the only version GitHub code scanning accepts).
135
+ * Paths are repo-relative with forward slashes -- path stability keeps
136
+ * alerts from churning. */
137
+ declare function toSarif(results: LintResult[], version: string, cwd?: string): string;
138
+
139
+ export { type Finding, KNOWN_EXTENSIONS, type LintOptions, type LintResult, type ParsedSkill, type Profile, RULES, type RuleMeta, SPEC_FIELDS, type Severity, type SkillFrontmatter, checkEvals, crossSkillFindings, evalsFileSchema, getRule, initEvals, lintSkillDir, parseSkillMd, toSarif };
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import {
2
+ KNOWN_EXTENSIONS,
3
+ RULES,
4
+ SPEC_FIELDS,
5
+ checkEvals,
6
+ crossSkillFindings,
7
+ evalsFileSchema,
8
+ getRule,
9
+ initEvals,
10
+ lintSkillDir,
11
+ parseSkillMd,
12
+ toSarif
13
+ } from "./chunk-MGSKVMAQ.js";
14
+ export {
15
+ KNOWN_EXTENSIONS,
16
+ RULES,
17
+ SPEC_FIELDS,
18
+ checkEvals,
19
+ crossSkillFindings,
20
+ evalsFileSchema,
21
+ getRule,
22
+ initEvals,
23
+ lintSkillDir,
24
+ parseSkillMd,
25
+ toSarif
26
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@sagargupta1610/skillcheck",
3
+ "version": "0.2.0",
4
+ "description": "Conformance suite for Agent Skills: lint SKILL.md against the spec, run it against real agent runtimes, publish a compatibility matrix",
5
+ "keywords": [
6
+ "agent-skills",
7
+ "skill",
8
+ "SKILL.md",
9
+ "claude-code",
10
+ "conformance",
11
+ "lint",
12
+ "agents"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Sagar Gupta <sg85207@gmail.com> (https://github.com/Sagargupta16)",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Sagargupta16/skillcheck.git"
19
+ },
20
+ "type": "module",
21
+ "bin": {
22
+ "skillcheck": "./dist/cli.js"
23
+ },
24
+ "main": "./dist/index.js",
25
+ "exports": {
26
+ ".": "./dist/index.js"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "engines": {
32
+ "node": ">=22"
33
+ },
34
+ "packageManager": "pnpm@11.10.0",
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsup --watch",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "lint": "biome check .",
41
+ "lint:fix": "biome check --write .",
42
+ "typecheck": "tsc --noEmit",
43
+ "prepublishOnly": "pnpm build"
44
+ },
45
+ "dependencies": {
46
+ "commander": "^15.0.0",
47
+ "glob": "^13.0.6",
48
+ "yaml": "^2.9.0",
49
+ "zod": "^4.4.3"
50
+ },
51
+ "devDependencies": {
52
+ "@biomejs/biome": "^2.5.2",
53
+ "@types/node": "^26.1.0",
54
+ "tsup": "^8.5.1",
55
+ "typescript": "^6.0.3",
56
+ "vitest": "^4.1.9"
57
+ }
58
+ }