skanvil 1.0.0 → 1.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.
- package/ARCHITECTURE.md +3 -1
- package/CHANGELOG.md +24 -4
- package/README.md +16 -11
- package/agent-skill/skanvil/SKILL.md +24 -13
- package/agent-skill/skanvil/references/REFERENCE.md +38 -12
- package/dist/{chunk-TYO4LLN5.js → chunk-WXEWECJP.js} +658 -133
- package/dist/cli.js +22 -3
- package/dist/index.d.ts +180 -1
- package/dist/index.js +19 -1
- package/docs/commands.md +23 -11
- package/docs/getting-started.md +46 -0
- package/docs/params-and-exports.md +196 -0
- package/docs/sync.md +4 -0
- package/docs/validation.md +26 -24
- package/package.json +1 -1
- package/templates/library/README.md +13 -0
- package/templates/library/gitignore +1 -0
- package/templates/library/package.json +21 -2
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
RULES,
|
|
6
6
|
TARGET_PRESETS,
|
|
7
7
|
addCommand,
|
|
8
|
+
buildLibrary,
|
|
8
9
|
collectSkills,
|
|
9
10
|
createLibrary,
|
|
10
11
|
displayPath,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
listCommand,
|
|
21
22
|
openLibrary,
|
|
22
23
|
packageVersion,
|
|
24
|
+
parseParamPairs,
|
|
23
25
|
printValidation,
|
|
24
26
|
rebuildIndex,
|
|
25
27
|
removeSkill,
|
|
@@ -31,7 +33,7 @@ import {
|
|
|
31
33
|
toKebabCase,
|
|
32
34
|
toTitleCase,
|
|
33
35
|
validateLibrary
|
|
34
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-WXEWECJP.js";
|
|
35
37
|
|
|
36
38
|
// src/cli.ts
|
|
37
39
|
import { Command, Option } from "commander";
|
|
@@ -170,7 +172,8 @@ async function syncCommand(dest, options) {
|
|
|
170
172
|
force: options.force,
|
|
171
173
|
categories: splitList(options.category),
|
|
172
174
|
skills: splitList(options.skill),
|
|
173
|
-
scope: options.global ? "user" : "project"
|
|
175
|
+
scope: options.global ? "user" : "project",
|
|
176
|
+
params: options.param?.length ? parseParamPairs(options.param) : void 0
|
|
174
177
|
});
|
|
175
178
|
if (options.json) {
|
|
176
179
|
console.log(JSON.stringify({ dryRun: Boolean(options.dryRun), mode, results }, null, 2));
|
|
@@ -188,6 +191,21 @@ async function syncCommand(dest, options) {
|
|
|
188
191
|
}
|
|
189
192
|
return 0;
|
|
190
193
|
}
|
|
194
|
+
async function buildCommand(options) {
|
|
195
|
+
const library = await openLibrary(options);
|
|
196
|
+
const out = new Output("build", options.json);
|
|
197
|
+
const result = await buildLibrary(library, { outDir: options.out, force: options.force });
|
|
198
|
+
if (options.json) {
|
|
199
|
+
console.log(JSON.stringify(result, null, 2));
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
out.success(`Built ${result.skills.length} skill(s) into ${displayPath(result.outDir)}`);
|
|
203
|
+
for (const skill of result.skills) {
|
|
204
|
+
const params = skill.params.length ? ` (params: ${skill.params.join(", ")})` : "";
|
|
205
|
+
out.print(` ${skill.exportName} ${skill.name}${params}`);
|
|
206
|
+
}
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
191
209
|
|
|
192
210
|
// src/commands/wizard.ts
|
|
193
211
|
import fs from "fs/promises";
|
|
@@ -747,6 +765,7 @@ ${Object.entries(RULES).map(([id, rule]) => ` ${id.padEnd(22)} ${rule.severity.
|
|
|
747
765
|
program2.command("doctor").description("diagnose the library and optionally repair fixable problems").option("--fix", "apply automatic fixes (README entries, name mismatch, tabs, tools -> allowed-tools)").addOption(cwdOption()).addOption(jsonOption()).action(run((options) => doctorCommand(options)));
|
|
748
766
|
const templates = program2.command("templates").description("manage skill templates");
|
|
749
767
|
templates.command("eject").description("copy the built-in skill template into the library for customisation").option("--category <id>", "eject as a category-specific template (skill-<id>)").option("--force", "overwrite an existing template folder").addOption(cwdOption()).addOption(jsonOption()).action(run((options) => ejectCommand(options)));
|
|
768
|
+
program2.command("build").description("generate the library's npm exports: one typed function per skill (dist/index.mjs, index.cjs, .d.ts)").option("--out <dir>", "output folder relative to the library root (default: dist)").option("--force", "build even when validation reports errors").addOption(cwdOption()).addOption(jsonOption()).action(run((options) => buildCommand(options)));
|
|
750
769
|
program2.command("install-skill [dest]").description("install or update the skanvil agent skill so coding agents in a project know how to use skanvil").option(
|
|
751
770
|
"--target <name>",
|
|
752
771
|
`preset (${Object.keys(TARGET_PRESETS).join(", ")}) or path relative to dest; repeatable or comma-separated (default: claude)`,
|
|
@@ -756,7 +775,7 @@ ${Object.entries(RULES).map(([id, rule]) => ` ${id.padEnd(22)} ${rule.severity.
|
|
|
756
775
|
"--target <name>",
|
|
757
776
|
`preset (${Object.keys(TARGET_PRESETS).join(", ")}) or path relative to dest; repeatable or comma-separated`,
|
|
758
777
|
collect
|
|
759
|
-
).option("--from <pkg|path>", "skills library to install (npm package in node_modules or folder); default: --cwd").option("--link", "symlink skills instead of copying (junctions on Windows)").option("--copy", "copy skills (overrides sync.mode = link)").option("--category <id>", "only skills of this category (repeatable)", collect).option("--skill <name>", "only this skill (repeatable)", collect).option("--force", "also sync skills with validation errors").option("--dry-run", "show what would change without writing").option("--global", "install into your user folders (~/.claude/skills, ~/.copilot/skills, ~/.agents/skills)").addOption(cwdOption()).addOption(jsonOption()).action(run((dest, options) => syncCommand(dest, options)));
|
|
778
|
+
).option("--from <pkg|path>", "skills library to install (npm package in node_modules or folder); default: --cwd").option("--link", "symlink skills instead of copying (junctions on Windows)").option("--copy", "copy skills (overrides sync.mode = link)").option("--category <id>", "only skills of this category (repeatable)", collect).option("--skill <name>", "only this skill (repeatable)", collect).option("--force", "also sync skills with validation errors").option("--dry-run", "show what would change without writing").option("--param <name=value>", "value for parametrized skills (repeatable); remembered per target", collect).option("--global", "install into your user folders (~/.claude/skills, ~/.copilot/skills, ~/.agents/skills)").addOption(cwdOption()).addOption(jsonOption()).action(run((dest, options) => syncCommand(dest, options)));
|
|
760
779
|
return program2;
|
|
761
780
|
}
|
|
762
781
|
var argv = process.argv.slice(2);
|
package/dist/index.d.ts
CHANGED
|
@@ -270,6 +270,104 @@ declare class Categories {
|
|
|
270
270
|
buildTitle(type: unknown, rawName: unknown, explicitTitle?: unknown): string;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/** File that declares a skill's parameters, next to its `SKILL.md`. Not part of the Agent Skills spec. */
|
|
274
|
+
declare const PARAMS_FILE = "params.json";
|
|
275
|
+
declare const ParamSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
276
|
+
type: z.ZodLiteral<"enum">;
|
|
277
|
+
values: z.ZodArray<z.ZodString>;
|
|
278
|
+
default: z.ZodString;
|
|
279
|
+
description: z.ZodOptional<z.ZodString>;
|
|
280
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
281
|
+
type: z.ZodLiteral<"string">;
|
|
282
|
+
default: z.ZodString;
|
|
283
|
+
description: z.ZodOptional<z.ZodString>;
|
|
284
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
285
|
+
type: z.ZodLiteral<"boolean">;
|
|
286
|
+
default: z.ZodBoolean;
|
|
287
|
+
description: z.ZodOptional<z.ZodString>;
|
|
288
|
+
}, z.core.$strict>]>;
|
|
289
|
+
/** One parameter definition. Every parameter has a default, so skills always render. */
|
|
290
|
+
type ParamDefinition = z.infer<typeof ParamSchema>;
|
|
291
|
+
/** Parameter definitions of a skill, by name. */
|
|
292
|
+
type ParamDefinitions = Record<string, ParamDefinition>;
|
|
293
|
+
/** Parameter values, as passed by users (`--param key=value` gives strings). */
|
|
294
|
+
type ParamValues = Record<string, string | boolean>;
|
|
295
|
+
/**
|
|
296
|
+
* Parses and checks the content of a `params.json`.
|
|
297
|
+
*
|
|
298
|
+
* @param text - File content.
|
|
299
|
+
* @returns The definitions.
|
|
300
|
+
* @throws Error describing the first problems: invalid JSON, unknown fields, missing defaults,
|
|
301
|
+
* or an enum default that is not one of its values.
|
|
302
|
+
*/
|
|
303
|
+
declare function parseParams(text: string): ParamDefinitions;
|
|
304
|
+
/**
|
|
305
|
+
* Reads a skill folder's `params.json`.
|
|
306
|
+
*
|
|
307
|
+
* @param skillDir - Absolute skill folder.
|
|
308
|
+
* @returns The definitions, or `null` when the skill has no `params.json`.
|
|
309
|
+
* @throws Error when the file exists but is invalid (see {@link parseParams}).
|
|
310
|
+
*/
|
|
311
|
+
declare function readParams(skillDir: string): Promise<ParamDefinitions | null>;
|
|
312
|
+
/**
|
|
313
|
+
* Checks values against definitions and fills in defaults.
|
|
314
|
+
*
|
|
315
|
+
* Strings `"true"` / `"false"` are accepted for boolean parameters, so CLI values work.
|
|
316
|
+
*
|
|
317
|
+
* @param definitions - The skill's parameters.
|
|
318
|
+
* @param given - Values to apply. @defaultValue `{}`
|
|
319
|
+
* @param skill - Skill name used in error messages.
|
|
320
|
+
* @param ignoreUnknown - Ignore values for parameters this skill does not declare (used when one
|
|
321
|
+
* set of values is applied to many skills). @defaultValue `false`
|
|
322
|
+
* @returns A value for every declared parameter.
|
|
323
|
+
* @throws Error for an unknown parameter (unless ignored), an enum value outside its list, or a
|
|
324
|
+
* value of the wrong type.
|
|
325
|
+
*
|
|
326
|
+
* @remarks Self-contained on purpose: `skanvil build` embeds this function's source.
|
|
327
|
+
*/
|
|
328
|
+
declare function resolveParamValues(definitions: ParamDefinitions, given?: Record<string, unknown>, skill?: string, ignoreUnknown?: boolean): ParamValues;
|
|
329
|
+
/**
|
|
330
|
+
* Renders a skill template.
|
|
331
|
+
*
|
|
332
|
+
* Syntax:
|
|
333
|
+
* - `{{name}}` is replaced by the value of a declared parameter. Names that are not parameters are
|
|
334
|
+
* left untouched, so literal `{{…}}` text in a skill without parameters is safe.
|
|
335
|
+
* - `{{#if name}}`, `{{#if !name}}`, `{{#if name == "value"}}` and `{{#if name != "value"}}`
|
|
336
|
+
* (single or double quotes) start a block, with an optional `{{else}}`, closed by `{{/if}}`.
|
|
337
|
+
* Blocks nest. A tag alone on its line removes the whole line, so blocks leave no blank lines.
|
|
338
|
+
*
|
|
339
|
+
* @param text - Template (LF or CRLF).
|
|
340
|
+
* @param values - Values for every parameter, normally from {@link resolveParamValues}.
|
|
341
|
+
* @returns The rendered text.
|
|
342
|
+
* @throws Error for an `{{#if}}` on an unknown parameter, a malformed condition, or unbalanced
|
|
343
|
+
* `{{#if}}` / `{{else}}` / `{{/if}}` tags.
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```ts
|
|
347
|
+
* renderTemplate('Use {{tracker}}.\n{{#if tracker == "jira"}}\nAdd the key.\n{{/if}}\n', { tracker: "jira" });
|
|
348
|
+
* // "Use jira.\nAdd the key.\n"
|
|
349
|
+
* ```
|
|
350
|
+
*
|
|
351
|
+
* @remarks Self-contained on purpose: `skanvil build` embeds this function's source.
|
|
352
|
+
*/
|
|
353
|
+
declare function renderTemplate(text: string, values: Record<string, string | boolean>): string;
|
|
354
|
+
/**
|
|
355
|
+
* Lists the `{{name}}` placeholders of a template that are not declared parameters.
|
|
356
|
+
*
|
|
357
|
+
* @param text - Template.
|
|
358
|
+
* @param definitions - Declared parameters.
|
|
359
|
+
* @returns Unique undeclared names, in order of appearance.
|
|
360
|
+
*/
|
|
361
|
+
declare function findUndeclaredParams(text: string, definitions: ParamDefinitions): string[];
|
|
362
|
+
/**
|
|
363
|
+
* Parses `key=value` pairs (from repeated `--param` options).
|
|
364
|
+
*
|
|
365
|
+
* @param pairs - Entries such as `ticketTracker=jira`; values may contain `=`.
|
|
366
|
+
* @returns The values (strings; booleans are converted by {@link resolveParamValues}).
|
|
367
|
+
* @throws Error for an entry without `=` or with an invalid name.
|
|
368
|
+
*/
|
|
369
|
+
declare function parseParamPairs(pairs: string[]): Record<string, string>;
|
|
370
|
+
|
|
273
371
|
/**
|
|
274
372
|
* Reading and editing `SKILL.md` files.
|
|
275
373
|
*
|
|
@@ -471,6 +569,25 @@ declare class Library {
|
|
|
471
569
|
* @throws The file system error when the file cannot be read.
|
|
472
570
|
*/
|
|
473
571
|
readSkill(name: string): Promise<SkillFile>;
|
|
572
|
+
/**
|
|
573
|
+
* Reads a skill's parameter definitions (`params.json`).
|
|
574
|
+
*
|
|
575
|
+
* @param name - Skill folder name.
|
|
576
|
+
* @returns The definitions, or `null` when the skill has none.
|
|
577
|
+
* @throws Error when `params.json` exists but is invalid.
|
|
578
|
+
*/
|
|
579
|
+
readSkillParams(name: string): Promise<ParamDefinitions | null>;
|
|
580
|
+
/**
|
|
581
|
+
* Reads a skill's `SKILL.md` rendered with parameter values. Skills without `params.json` are
|
|
582
|
+
* returned as they are.
|
|
583
|
+
*
|
|
584
|
+
* @param name - Skill folder name.
|
|
585
|
+
* @param values - Parameter values; missing ones take their defaults. @defaultValue `{}`
|
|
586
|
+
* @returns The rendered file.
|
|
587
|
+
* @throws Error when the file cannot be read, `params.json` is invalid, a value is invalid, or
|
|
588
|
+
* the template is malformed.
|
|
589
|
+
*/
|
|
590
|
+
readSkillRendered(name: string, values?: Record<string, unknown>): Promise<SkillFile>;
|
|
474
591
|
/**
|
|
475
592
|
* Writes a skill's `SKILL.md`, keeping the line ending it was read with.
|
|
476
593
|
*
|
|
@@ -567,6 +684,12 @@ interface SyncOptions {
|
|
|
567
684
|
scope?: TargetScope;
|
|
568
685
|
/** Home folder for the `user` scope. @defaultValue `os.homedir()` */
|
|
569
686
|
homeDir?: string;
|
|
687
|
+
/**
|
|
688
|
+
* Values for parametrized skills (`params.json`), applied to every skill that declares them.
|
|
689
|
+
* When omitted, each target reuses the values of its previous sync, then the defaults.
|
|
690
|
+
* Every key must be declared by at least one synced skill.
|
|
691
|
+
*/
|
|
692
|
+
params?: Record<string, string | boolean>;
|
|
570
693
|
}
|
|
571
694
|
/** Outcome of {@link syncSkills} for one target. */
|
|
572
695
|
interface TargetResult {
|
|
@@ -589,6 +712,8 @@ interface TargetResult {
|
|
|
589
712
|
}[];
|
|
590
713
|
/** Non-fatal problems, such as a link that fell back to a copy. */
|
|
591
714
|
warnings: string[];
|
|
715
|
+
/** Parameter values used for this target (empty when no synced skill has parameters). */
|
|
716
|
+
params: Record<string, string | boolean>;
|
|
592
717
|
}
|
|
593
718
|
/**
|
|
594
719
|
* Resolves a sync target to an absolute folder.
|
|
@@ -689,6 +814,60 @@ interface AgentSkillResult {
|
|
|
689
814
|
*/
|
|
690
815
|
declare function installAgentSkill(options: InstallAgentSkillOptions): Promise<AgentSkillResult[]>;
|
|
691
816
|
|
|
817
|
+
/** Options of {@link buildLibrary}. */
|
|
818
|
+
interface BuildOptions {
|
|
819
|
+
/** Output folder, relative to the library root. @defaultValue `"dist"` */
|
|
820
|
+
outDir?: string;
|
|
821
|
+
/** Build even when validation reports errors. @defaultValue `false` */
|
|
822
|
+
force?: boolean;
|
|
823
|
+
}
|
|
824
|
+
/** One exported skill. */
|
|
825
|
+
interface BuiltSkill {
|
|
826
|
+
/** Skill folder name. */
|
|
827
|
+
name: string;
|
|
828
|
+
/** Name of the exported function. */
|
|
829
|
+
exportName: string;
|
|
830
|
+
/** Category id. */
|
|
831
|
+
category: string;
|
|
832
|
+
/** Declared parameter names. */
|
|
833
|
+
params: string[];
|
|
834
|
+
}
|
|
835
|
+
/** Result of {@link buildLibrary}. */
|
|
836
|
+
interface BuildResult {
|
|
837
|
+
/** Absolute output folder. */
|
|
838
|
+
outDir: string;
|
|
839
|
+
/** Generated files, relative to the library root (posix). */
|
|
840
|
+
files: string[];
|
|
841
|
+
/** Exported skills, in folder order. */
|
|
842
|
+
skills: BuiltSkill[];
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Chooses export names: the folder name without its category prefix, in camelCase
|
|
846
|
+
* (`front-api-review` becomes `apiReview`). Names that would collide fall back to the full folder
|
|
847
|
+
* name (`frontReview`, `backReview`), and invalid or reserved identifiers get a `skill` prefix.
|
|
848
|
+
*
|
|
849
|
+
* @param library - Library whose categories define the prefixes.
|
|
850
|
+
* @param names - Skill folder names.
|
|
851
|
+
* @returns Export name by folder name.
|
|
852
|
+
*/
|
|
853
|
+
declare function exportNames(library: Library, names: string[]): Map<string, string>;
|
|
854
|
+
/**
|
|
855
|
+
* Generates `index.mjs`, `index.cjs`, `index.d.mts` and `index.d.cts` for the library's
|
|
856
|
+
* publishable skills (categories with `validated: true`).
|
|
857
|
+
*
|
|
858
|
+
* @param library - Library to build.
|
|
859
|
+
* @param options - Output folder and error handling.
|
|
860
|
+
* @returns What was generated.
|
|
861
|
+
* @throws Error when validation reports errors (unless `force`), or a skill's parameters are invalid.
|
|
862
|
+
*
|
|
863
|
+
* @example
|
|
864
|
+
* ```ts
|
|
865
|
+
* const { skills } = await buildLibrary(await Library.open("."));
|
|
866
|
+
* // then, in a consumer: import { apiReview } from "@acme/my-skills";
|
|
867
|
+
* ```
|
|
868
|
+
*/
|
|
869
|
+
declare function buildLibrary(library: Library, options?: BuildOptions): Promise<BuildResult>;
|
|
870
|
+
|
|
692
871
|
/** Severity of a reported diagnostic (rules set to `off` report nothing). */
|
|
693
872
|
type DiagnosticSeverity = Exclude<Severity, "off">;
|
|
694
873
|
/** A single validation finding. */
|
|
@@ -1079,4 +1258,4 @@ interface ListedSkill {
|
|
|
1079
1258
|
*/
|
|
1080
1259
|
declare function collectSkills(library: Library, category?: string): Promise<ListedSkill[]>;
|
|
1081
1260
|
|
|
1082
|
-
export { AGENT_SKILL_NAME, type AddSkillOptions, type AgentSkillResult, CONFIG_FILE, Categories, type Category, ConfigSchema, type CreateLibraryOptions, DEFAULT_CATEGORIES, type Diagnostic, type DoctorResult, type EditSkillOptions, type InstallAgentSkillOptions, Library, type ListedSkill, MANIFEST_FILE, RULES, type RawConfig, type ResolveTargetOptions, type ResolvedConfig, type Severity, SkillFile, type SkillInfo, type SyncOptions, TARGET_PRESETS, type TargetResult, type TargetScope, USER_TARGET_PRESETS, type ValidationResult, agentSkillSource, collectSkills, configJsonSchema, createLibrary, doctor, editSkill, ejectSkillTemplate, formatDiagnostic, installAgentSkill, loadConfig, parseProfile, rebuildIndex, reconcileIndex, removeSkill, resolveConfig, resolveSkillTemplate, resolveTarget, scaffoldSkill, syncSkills, validateLibrary };
|
|
1261
|
+
export { AGENT_SKILL_NAME, type AddSkillOptions, type AgentSkillResult, type BuildOptions, type BuildResult, type BuiltSkill, CONFIG_FILE, Categories, type Category, ConfigSchema, type CreateLibraryOptions, DEFAULT_CATEGORIES, type Diagnostic, type DoctorResult, type EditSkillOptions, type InstallAgentSkillOptions, Library, type ListedSkill, MANIFEST_FILE, PARAMS_FILE, type ParamDefinition, type ParamDefinitions, type ParamValues, RULES, type RawConfig, type ResolveTargetOptions, type ResolvedConfig, type Severity, SkillFile, type SkillInfo, type SyncOptions, TARGET_PRESETS, type TargetResult, type TargetScope, USER_TARGET_PRESETS, type ValidationResult, agentSkillSource, buildLibrary, collectSkills, configJsonSchema, createLibrary, doctor, editSkill, ejectSkillTemplate, exportNames, findUndeclaredParams, formatDiagnostic, installAgentSkill, loadConfig, parseParamPairs, parseParams, parseProfile, readParams, rebuildIndex, reconcileIndex, removeSkill, renderTemplate, resolveConfig, resolveParamValues, resolveSkillTemplate, resolveTarget, scaffoldSkill, syncSkills, validateLibrary };
|
package/dist/index.js
CHANGED
|
@@ -6,31 +6,40 @@ import {
|
|
|
6
6
|
DEFAULT_CATEGORIES,
|
|
7
7
|
Library,
|
|
8
8
|
MANIFEST_FILE,
|
|
9
|
+
PARAMS_FILE,
|
|
9
10
|
RULES,
|
|
10
11
|
SkillFile,
|
|
11
12
|
TARGET_PRESETS,
|
|
12
13
|
USER_TARGET_PRESETS,
|
|
13
14
|
agentSkillSource,
|
|
15
|
+
buildLibrary,
|
|
14
16
|
collectSkills,
|
|
15
17
|
configJsonSchema,
|
|
16
18
|
createLibrary,
|
|
17
19
|
doctor,
|
|
18
20
|
editSkill,
|
|
19
21
|
ejectSkillTemplate,
|
|
22
|
+
exportNames,
|
|
23
|
+
findUndeclaredParams,
|
|
20
24
|
formatDiagnostic,
|
|
21
25
|
installAgentSkill,
|
|
22
26
|
loadConfig,
|
|
27
|
+
parseParamPairs,
|
|
28
|
+
parseParams,
|
|
23
29
|
parseProfile,
|
|
30
|
+
readParams,
|
|
24
31
|
rebuildIndex,
|
|
25
32
|
reconcileIndex,
|
|
26
33
|
removeSkill,
|
|
34
|
+
renderTemplate,
|
|
27
35
|
resolveConfig,
|
|
36
|
+
resolveParamValues,
|
|
28
37
|
resolveSkillTemplate,
|
|
29
38
|
resolveTarget,
|
|
30
39
|
scaffoldSkill,
|
|
31
40
|
syncSkills,
|
|
32
41
|
validateLibrary
|
|
33
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-WXEWECJP.js";
|
|
34
43
|
export {
|
|
35
44
|
AGENT_SKILL_NAME,
|
|
36
45
|
CONFIG_FILE,
|
|
@@ -39,25 +48,34 @@ export {
|
|
|
39
48
|
DEFAULT_CATEGORIES,
|
|
40
49
|
Library,
|
|
41
50
|
MANIFEST_FILE,
|
|
51
|
+
PARAMS_FILE,
|
|
42
52
|
RULES,
|
|
43
53
|
SkillFile,
|
|
44
54
|
TARGET_PRESETS,
|
|
45
55
|
USER_TARGET_PRESETS,
|
|
46
56
|
agentSkillSource,
|
|
57
|
+
buildLibrary,
|
|
47
58
|
collectSkills,
|
|
48
59
|
configJsonSchema,
|
|
49
60
|
createLibrary,
|
|
50
61
|
doctor,
|
|
51
62
|
editSkill,
|
|
52
63
|
ejectSkillTemplate,
|
|
64
|
+
exportNames,
|
|
65
|
+
findUndeclaredParams,
|
|
53
66
|
formatDiagnostic,
|
|
54
67
|
installAgentSkill,
|
|
55
68
|
loadConfig,
|
|
69
|
+
parseParamPairs,
|
|
70
|
+
parseParams,
|
|
56
71
|
parseProfile,
|
|
72
|
+
readParams,
|
|
57
73
|
rebuildIndex,
|
|
58
74
|
reconcileIndex,
|
|
59
75
|
removeSkill,
|
|
76
|
+
renderTemplate,
|
|
60
77
|
resolveConfig,
|
|
78
|
+
resolveParamValues,
|
|
61
79
|
resolveSkillTemplate,
|
|
62
80
|
resolveTarget,
|
|
63
81
|
scaffoldSkill,
|
package/docs/commands.md
CHANGED
|
@@ -176,17 +176,18 @@ Copy the built-in skill template into the library so you can edit it.
|
|
|
176
176
|
|
|
177
177
|
Install the library's skills into a project. `dest` defaults to the current directory. See [Sync](sync.md).
|
|
178
178
|
|
|
179
|
-
| Option
|
|
180
|
-
|
|
|
181
|
-
| `--target <name>`
|
|
182
|
-
| `--from <pkg\|path>`
|
|
183
|
-
| `--link`
|
|
184
|
-
| `--copy`
|
|
185
|
-
| `--category <id>`
|
|
186
|
-
| `--skill <name>`
|
|
187
|
-
| `--force`
|
|
188
|
-
| `--dry-run`
|
|
189
|
-
| `--
|
|
179
|
+
| Option | Description |
|
|
180
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
181
|
+
| `--target <name>` | `claude`, `copilot`, `agents`, `codex`, or a path relative to `dest`. Repeatable and comma-separated. Default: `sync.targets` from the config, or `claude`. |
|
|
182
|
+
| `--from <pkg\|path>` | Source library: a folder, or an npm package installed in a `node_modules` above `dest`. Default: `--cwd`. |
|
|
183
|
+
| `--link` | Symlink instead of copying (junctions on Windows). |
|
|
184
|
+
| `--copy` | Copy, even if the config sets `sync.mode: "link"`. |
|
|
185
|
+
| `--category <id>` | Only sync this category. Repeatable. Disables pruning. |
|
|
186
|
+
| `--skill <name>` | Only sync this skill. Repeatable. Disables pruning. |
|
|
187
|
+
| `--force` | Also sync skills that have validation errors. |
|
|
188
|
+
| `--dry-run` | Show what would change without writing. |
|
|
189
|
+
| `--param <name=value>` | Value for [parametrized skills](params-and-exports.md). Repeatable. It applies to every synced skill that declares it, and an unknown name is an error. Remembered per target. |
|
|
190
|
+
| `--global` | Install into your user folders instead of a project (see below). Do not pass `dest` with it. |
|
|
190
191
|
|
|
191
192
|
Custom `--target` paths must resolve to a folder inside `dest`: `../shared` or `.` are rejected before anything is written. With `--global`, only presets are accepted:
|
|
192
193
|
|
|
@@ -200,6 +201,17 @@ skanvil sync --from @acme/my-skills --target agents --link
|
|
|
200
201
|
skanvil sync -C ./my-skills --global --target claude
|
|
201
202
|
```
|
|
202
203
|
|
|
204
|
+
## `build`
|
|
205
|
+
|
|
206
|
+
Generate the library's npm exports in `dist/`: `index.mjs`, `index.cjs` and their type declarations. They contain one function per publishable skill that returns its `SKILL.md` (rendered with parameters), a `skills` list and `getSkill(name)`. See [Parameters and exports](params-and-exports.md).
|
|
207
|
+
|
|
208
|
+
| Option | Description |
|
|
209
|
+
| ------------- | ------------------------------------------------------------ |
|
|
210
|
+
| `--out <dir>` | Output folder relative to the library root. Default: `dist`. |
|
|
211
|
+
| `--force` | Build even when validation reports errors. |
|
|
212
|
+
|
|
213
|
+
`--json` prints `{ outDir, files, skills: [{ name, exportName, category, params }] }`.
|
|
214
|
+
|
|
203
215
|
## `install-skill [dest]`
|
|
204
216
|
|
|
205
217
|
Install or update the `skanvil` agent skill in a project, so coding agents there know how to use skanvil: which commands to run, the golden rules, and how to fix each diagnostic. `dest` defaults to the current directory.
|
package/docs/getting-started.md
CHANGED
|
@@ -4,6 +4,52 @@ This guide creates a skills library, adds and edits skills, checks them in CI, p
|
|
|
4
4
|
|
|
5
5
|
> **Prefer menus?** Run `npx skanvil` without arguments in a terminal. The [wizard](commands.md#wizard) walks through every step below. After each action it prints the equivalent command, which is the one this guide uses.
|
|
6
6
|
|
|
7
|
+
## 0. Let your AI agent learn skanvil (optional)
|
|
8
|
+
|
|
9
|
+
skanvil ships an agent skill that teaches coding agents (Claude Code, GitHub Copilot, Codex…) how to use it:
|
|
10
|
+
|
|
11
|
+
- the commands and the rules (never rename folders or edit the README index by hand);
|
|
12
|
+
- how to fix every validation error;
|
|
13
|
+
- how to write good skill content.
|
|
14
|
+
|
|
15
|
+
Paste this prompt into your agent, inside the repository where you want to work. That can be an existing skills library, an application that consumes one, or an empty folder for a new library.
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Install and learn the skanvil agent skill in this repository.
|
|
19
|
+
|
|
20
|
+
1. Work out which skills folder you read: Claude Code uses .claude/skills, GitHub Copilot
|
|
21
|
+
.github/skills, Codex .agents/skills. Then run the matching command, for example:
|
|
22
|
+
npx skanvil@latest install-skill --target claude
|
|
23
|
+
Use --target copilot or --target agents (comma-separated for several) if that fits you
|
|
24
|
+
better. Do not use --global unless I ask for it.
|
|
25
|
+
2. Read the installed <skills folder>/skanvil/SKILL.md completely (for example
|
|
26
|
+
.claude/skills/skanvil/SKILL.md), and then its references/REFERENCE.md.
|
|
27
|
+
3. Detect the context:
|
|
28
|
+
- if there is a .skanvilrc.json, run `npx skanvil list --json` and `npx skanvil validate --json`;
|
|
29
|
+
- otherwise, tell me this is not a skills library yet.
|
|
30
|
+
4. Reply with:
|
|
31
|
+
- a short summary of how you will use skanvil here;
|
|
32
|
+
- the golden rules you will follow;
|
|
33
|
+
- what the library contains and any validation problems you found.
|
|
34
|
+
Do not change any skill until I ask.
|
|
35
|
+
|
|
36
|
+
From now on, whenever I ask you to create, rename, move, delete, validate, parametrize, build or
|
|
37
|
+
install skills, use the skanvil skill and its commands. Do not edit skill folders or the README
|
|
38
|
+
index by hand, always pass every option (the commands are interactive otherwise), and never run
|
|
39
|
+
`skanvil` without arguments, because that opens a menu meant for humans.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
After that, you can ask in plain language, and the agent will turn each request into the right commands. For example:
|
|
43
|
+
|
|
44
|
+
- _"Create a skills library called acme-skills with categories mobile and data."_
|
|
45
|
+
- _"Add a backend skill for zero-downtime database migrations and write its content."_
|
|
46
|
+
- _"Make the code-review skill configurable for GitHub or Jira."_
|
|
47
|
+
- _"Install the library's skills into ../my-app for Claude Code and Copilot."_
|
|
48
|
+
|
|
49
|
+
The agent will then run `init`, `add`, `edit`, `validate`, `sync --param`, `build` and so on, and check the result with `skanvil validate`.
|
|
50
|
+
|
|
51
|
+
`skanvil init` already installs this skill in new libraries, in `.claude/skills/skanvil` by default. Run the prompt again after upgrading skanvil, and `install-skill` will refresh the skill.
|
|
52
|
+
|
|
7
53
|
## 1. Create a library
|
|
8
54
|
|
|
9
55
|
```bash
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Parameters and package exports
|
|
2
|
+
|
|
3
|
+
Two optional features turn a skills library into something other code can use.
|
|
4
|
+
|
|
5
|
+
- **Parameters** make one skill adapt to each team or project: the issue tracker, the package
|
|
6
|
+
manager, a dashboard URL. You keep one skill instead of near-identical copies.
|
|
7
|
+
- **Exports** (`skanvil build`) make the library's npm package importable:
|
|
8
|
+
`import { codeReview } from "@acme/skills"` returns the skill's `SKILL.md`, rendered with the
|
|
9
|
+
parameters you pass. The result is fully typed.
|
|
10
|
+
|
|
11
|
+
Skills without parameters work exactly as before. You can use exports without ever adding a
|
|
12
|
+
parameter.
|
|
13
|
+
|
|
14
|
+
## When this is worth it
|
|
15
|
+
|
|
16
|
+
- **One skill, many variants.** Teams use different trackers, CI systems or package managers, and
|
|
17
|
+
you are about to duplicate a skill to change a few lines.
|
|
18
|
+
- **Facts an agent cannot infer.** For example the Grafana URL, the incident Slack channel or the
|
|
19
|
+
real deploy command. A parameter keeps them out of the shared text.
|
|
20
|
+
- **Skills consumed by code**, such as a project generator that installs tailored skills, a custom
|
|
21
|
+
agent built with an SDK that puts a skill in its prompt, or a monorepo script that installs a
|
|
22
|
+
skill per package.
|
|
23
|
+
|
|
24
|
+
If every project uses the same tools, plain `SKILL.md` files plus `skanvil sync` are simpler.
|
|
25
|
+
|
|
26
|
+
## Declaring parameters
|
|
27
|
+
|
|
28
|
+
Add a `params.json` next to the skill's `SKILL.md`:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
skills/front-code-review/
|
|
32
|
+
├── SKILL.md
|
|
33
|
+
├── params.json
|
|
34
|
+
└── references/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"ticketTracker": {
|
|
40
|
+
"type": "enum",
|
|
41
|
+
"values": ["github", "jira", "linear"],
|
|
42
|
+
"default": "github",
|
|
43
|
+
"description": "Where findings are filed"
|
|
44
|
+
},
|
|
45
|
+
"packageManager": { "type": "string", "default": "npm" },
|
|
46
|
+
"strict": { "type": "boolean", "default": false, "description": "Block merges on any finding" }
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Rules for `params.json`:
|
|
51
|
+
|
|
52
|
+
- **Names** are camelCase letters and digits.
|
|
53
|
+
- **Types** are `enum` (with `values`), `string` or `boolean`.
|
|
54
|
+
- **Defaults** are required, so a skill always renders. An enum default must be one of its values.
|
|
55
|
+
- **The file stays out of agents' folders.** `params.json` is not part of the Agent Skills spec, so
|
|
56
|
+
`sync` and `build` never copy it to them.
|
|
57
|
+
|
|
58
|
+
## Template syntax
|
|
59
|
+
|
|
60
|
+
Use the parameters anywhere in `SKILL.md`, including the frontmatter:
|
|
61
|
+
|
|
62
|
+
```md
|
|
63
|
+
Run `{{packageManager}} test` before reviewing, and file each finding in {{ticketTracker}}.
|
|
64
|
+
|
|
65
|
+
{{#if ticketTracker == "jira"}}
|
|
66
|
+
Use the `PROJ-123` key format in the summary.
|
|
67
|
+
{{else}}
|
|
68
|
+
Reference issues as `#123`.
|
|
69
|
+
{{/if}}
|
|
70
|
+
|
|
71
|
+
{{#if strict}}
|
|
72
|
+
Block the merge until every finding is resolved.
|
|
73
|
+
{{/if}}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Syntax | Meaning |
|
|
77
|
+
| ----------------------------------------------------- | --------------------------------------------------------------------- |
|
|
78
|
+
| `{{name}}` | The parameter's value. |
|
|
79
|
+
| `{{#if name}}` / `{{#if !name}}` | The parameter is true, or false. For strings, "true" means not empty. |
|
|
80
|
+
| `{{#if name == "value"}}` / `{{#if name != "value"}}` | Equality test (single or double quotes). |
|
|
81
|
+
| `{{else}}`, `{{/if}}` | Else branch and end of block. Blocks can nest. |
|
|
82
|
+
|
|
83
|
+
How rendering behaves:
|
|
84
|
+
|
|
85
|
+
- **Tag lines vanish.** A block tag alone on its line removes the whole line, so blocks leave no
|
|
86
|
+
blank lines behind.
|
|
87
|
+
- **Literal braces are safe.** `{{text}}` that is not a declared parameter is left as it is. In a
|
|
88
|
+
skill that has a `params.json`, validation reports it, because it is usually a typo.
|
|
89
|
+
|
|
90
|
+
## Validation
|
|
91
|
+
|
|
92
|
+
`skanvil validate` checks parametrized skills as rendered with their defaults, so every spec and
|
|
93
|
+
library rule still applies. Two extra rules apply:
|
|
94
|
+
|
|
95
|
+
- **`params-file`**: `params.json` is valid.
|
|
96
|
+
- **`params-template`**: `{{…}}` references and `{{#if}}` blocks use declared parameters and are
|
|
97
|
+
balanced.
|
|
98
|
+
|
|
99
|
+
Fix the defaults first: the README index and `skanvil list` show the description rendered with
|
|
100
|
+
them.
|
|
101
|
+
|
|
102
|
+
## Installing with values (`sync --param`)
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx skanvil sync --from @acme/skills --target claude \
|
|
106
|
+
--param ticketTracker=jira --param packageManager=pnpm --param strict=true
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
What happens:
|
|
110
|
+
|
|
111
|
+
- **Rendering.** Every synced skill that declares a parameter gets that value. The others take
|
|
112
|
+
their defaults. The installed `SKILL.md` is rendered and `params.json` is left out.
|
|
113
|
+
- **Typos fail early.** A name that no synced skill declares is an error, raised before anything
|
|
114
|
+
is written.
|
|
115
|
+
- **Values are remembered per target**, in `.skanvil-sync.json`. A later `skanvil sync` without
|
|
116
|
+
`--param` reuses them, and passing `--param` again replaces them.
|
|
117
|
+
- **No links.** A rendered skill cannot be linked, so `--link` copies parametrized skills (with a
|
|
118
|
+
warning) and still links the others.
|
|
119
|
+
|
|
120
|
+
## Package exports (`skanvil build`)
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx skanvil build # writes dist/index.mjs, index.cjs, index.d.mts, index.d.cts
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Generated libraries already wire this up:
|
|
127
|
+
|
|
128
|
+
- `npm run build` runs `skanvil validate` and then `skanvil build`.
|
|
129
|
+
- `prepublishOnly` builds before every publish.
|
|
130
|
+
- `package.json` has `exports`, `main` and `types` pointing at `dist/`.
|
|
131
|
+
- `dist/` is gitignored.
|
|
132
|
+
|
|
133
|
+
The build refuses to run while validation reports errors, unless you pass `--force`.
|
|
134
|
+
|
|
135
|
+
For a library with `front-code-review` (with the parameters above) and `back-migrations`,
|
|
136
|
+
consumers get:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { codeReview, migrations, skills, getSkill, type CodeReviewParams } from "@acme/skills";
|
|
140
|
+
|
|
141
|
+
codeReview(); // SKILL.md with the defaults
|
|
142
|
+
codeReview({ ticketTracker: "jira", strict: true }); // rendered for one team
|
|
143
|
+
codeReview({ ticketTracker: "trello" }); // type error, and a runtime error in plain JS
|
|
144
|
+
|
|
145
|
+
migrations(); // a skill without parameters: its SKILL.md as is
|
|
146
|
+
|
|
147
|
+
for (const skill of skills) console.log(skill.name, skill.category, skill.description);
|
|
148
|
+
|
|
149
|
+
// Every file of a skill (references, scripts, assets), with SKILL.md rendered:
|
|
150
|
+
const files = getSkill("front-code-review").files({ ticketTracker: "linear" });
|
|
151
|
+
// { "SKILL.md": { encoding: "utf8", content: "…" }, "references/REFERENCE.md": { … }, … }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
How exports are named:
|
|
155
|
+
|
|
156
|
+
- **The default** is the folder name without its category prefix, in camelCase:
|
|
157
|
+
`front-code-review` becomes `codeReview`.
|
|
158
|
+
- **Clashes** use the full folder name, for example `frontReview` and `backReview`.
|
|
159
|
+
- **Names that cannot be identifiers** get a `skill` prefix, for example `skill2faSetup`.
|
|
160
|
+
- **What is exported:** only publishable skills, meaning categories with `validated: true`.
|
|
161
|
+
`_internal` skills are never exported.
|
|
162
|
+
|
|
163
|
+
The generated code has no dependencies, not even skanvil. It embeds the same renderer that
|
|
164
|
+
skanvil uses, so `codeReview({ … })` and `skanvil sync --param …` always produce the same text.
|
|
165
|
+
|
|
166
|
+
### Examples
|
|
167
|
+
|
|
168
|
+
**A project generator** writes tailored skills into a new service:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import fs from "node:fs";
|
|
172
|
+
import path from "node:path";
|
|
173
|
+
import { getSkill } from "@acme/skills";
|
|
174
|
+
|
|
175
|
+
function installSkill(name: string, params: Record<string, string | boolean>, root = ".claude/skills") {
|
|
176
|
+
for (const [file, { encoding, content }] of Object.entries(getSkill(name as never).files(params as never))) {
|
|
177
|
+
const target = path.join(root, name, file);
|
|
178
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
179
|
+
fs.writeFileSync(target, Buffer.from(content, encoding));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
installSkill("front-code-review", { ticketTracker: "jira", packageManager: "pnpm" });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**A custom agent** puts a skill in its system prompt:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { codeReview } from "@acme/skills";
|
|
190
|
+
|
|
191
|
+
const system = codeReview({ ticketTracker: "github", strict: true });
|
|
192
|
+
// pass `system` to your Claude API / Agent SDK call
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The skill is then versioned like any dependency, so Renovate or Dependabot tell you when it
|
|
196
|
+
changes.
|