/** * Skill packaging - bundle SKILL.md with all linked resources * * This module provides the unified packaging logic used by all flows: * - Direct packaging of existing SKILL.md files * - Post-processing after generating SKILL.md from agent.yml * * Package formats supported: * - directory: Ready-to-use directory structure * - zip: Single file archive (preferred for Windows compatibility) * - npm: Standard npm package with package.json * - marketplace: JSON manifest for plugin registries * * Uses ResourceRegistry + transformContent() from @vibe-agent-toolkit/resources * for link resolution and rewriting (replacing the previous inline regex approach). */ import { type AllowUsageLedger, type ValidationConfig, type ValidationIssue } from '@vibe-agent-toolkit/agent-schema'; import { ResourceRegistry } from '@vibe-agent-toolkit/resources'; import { type GitTracker } from '@vibe-agent-toolkit/utils'; import { type SkillFileEntry } from './files-config.js'; import { type DeclaredEvalSuite } from './test-input.js'; import { type PackagingValidationResult, type SkillPackagingConfig } from './validators/packaging-validator.js'; /** * Resource naming strategy type */ export type ResourceNamingStrategy = 'basename' | 'resource-id' | 'preserve-path'; /** * Packaging target: determines ZIP directory structure * - 'claude-code': Standard VAT format with resources/ subdirectory (default) * - 'claude-web': Claude.ai web upload format with references/, scripts/, assets/ subdirectories */ export type PackagingTarget = 'claude-code' | 'claude-web'; export interface PackageSkillOptions { /** * Output directory for packaged skill * Default: /dist/skills/ */ outputPath?: string; /** * Package format(s) to generate * Default: ['directory'] */ formats?: ('directory' | 'zip' | 'npm' | 'marketplace')[]; /** * Whether to rewrite links to be relative to package root * Default: true */ rewriteLinks?: boolean; /** * Base path for resolving relative links in SKILL.md * Default: dirname(skillPath) */ basePath?: string; /** * Strategy for naming packaged resource files * * - 'basename': Use original filename only (default, may cause conflicts) * - 'resource-id': Flatten path to kebab-case filename (descriptive, unique) * - 'preserve-path': Preserve directory structure in output * * Default: 'basename' * * @example * // Original: knowledge-base/guides/topics/quickstart/overview.md * // basename: overview.md (may conflict) * // resource-id: guides-topics-quickstart-overview.md (with stripPrefix: 'knowledge-base-') * // preserve-path: guides/topics/quickstart/overview.md (creates subdirectories) */ resourceNaming?: ResourceNamingStrategy; /** * Path prefix to strip before applying naming strategy * * Removes a directory prefix from the relative path before the naming strategy is applied. * Works with both 'resource-id' and 'preserve-path' strategies. * * @example * // Original: knowledge-base/guides/topics/quickstart/overview.md * // stripPrefix: 'knowledge-base' * // * // resource-id: guides-topics-quickstart-overview.md * // preserve-path: guides/topics/quickstart/overview.md */ stripPrefix?: string; /** How deep to follow markdown links (default: 2) */ linkFollowDepth?: number | 'full' | undefined; /** Whether to exclude navigation files (README.md, index.md, etc.) from bundle (default: true) */ excludeNavigationFiles?: boolean | undefined; /** Exclude patterns and rewrite templates for non-bundled links */ excludeReferencesFromBundle?: { rules?: Array<{ patterns: string[]; template?: string | undefined; }> | undefined; defaultTemplate?: string | undefined; } | undefined; /** * Pre-built ResourceRegistry for the project. * When provided, packageSkill() skips creating its own registry. * Used by packageSkills() to share a single registry across multiple skill builds. */ registry?: ResourceRegistry | undefined; /** * Pre-populated {@link GitTracker} for the containing repo. * * When supplied, gitignore checks during the link-graph walk become O(1) * active-set lookups instead of `git check-ignore` spawns. Used by batched * build paths (e.g. `vat skills build`) that already constructed a tracker * for discovery/scanning. */ gitTracker?: GitTracker | undefined; /** * Packaging target — controls the ZIP directory structure produced. * * - 'claude-code' (default): Standard VAT layout with resources/ subdirectory * - 'claude-web': Claude.ai web upload layout with references/, scripts/, assets/ subdirectories * * Default: 'claude-code' */ target?: PackagingTarget | undefined; /** * Explicit file mappings for build artifacts, unlinked files, or routing overrides. * * Each entry copies source to dest in the skill output. Links matching * files[].source are rewritten to dest. Links matching files[].dest are * left as-is (assumed to be build artifacts placed at dest during build). */ files?: SkillFileEntry[] | undefined; /** * Validation framework configuration: severity overrides and per-path allow entries. * See docs/validation-codes.md for codes and defaults. */ validation?: ValidationConfig | undefined; /** * Absolute directories holding this skill's DECLARED test input (its eval suite). * Links into them are excluded from the bundle, and anything that still reaches * the output emits `PACKAGED_TEST_INPUT`. Derived from the skill's `test.evals` * by {@link resolveTestInputDirs} — see test-input.ts for why test input must * never ship. */ testInputDirs?: string[] | undefined; /** * The RUN's allow-entry usage ledger. * * `validation.allow` is declared once per package but evaluated once per * skill AND once per lane, so "this entry matched nothing" is a question only * the whole invocation can answer. A caller that builds more than one skill, * or that validates the SOURCE tree before packaging (`vat build` does both), * MUST supply one ledger for the whole invocation and drain it itself with * `allowUnusedIssues()` after the last skill. * * Omitting it is a positive claim that THIS `packageSkill` call is the whole * run — true for single-skill library callers, who get the run-level verdict * folded into `postBuildIssues` here. It is false for anything that loops. */ allowLedger?: AllowUsageLedger | undefined; } /** * Map a merged {@link SkillPackagingConfig} onto {@link PackageSkillOptions}. * * The single canonical conversion used by BOTH `vat skills build` and * `vat skill test` (the pool build), so the dist a test exercises is byte-for-byte * what `vat skills build` would produce. `basePath` defaults to `dirname(skillPath)`. * * `projectSkills` is EVERY skill the project declares, with its effective packaging * config — assembled ONCE per invocation by the calling lane and passed down, never * recomputed per skill (a per-skill walk of the whole project config is an N+1 this * repo has been bitten by). It is required rather than defaulted because the test-input * rule is project-wide: omitting it silently packages another skill's eval answer key. * A lane with genuinely no project to enumerate passes `[]` explicitly. */ export declare function packagingConfigToPackageOptions(config: SkillPackagingConfig, anchors: { skillPath: string; outputPath: string; }, projectSkills: readonly DeclaredEvalSuite[]): PackageSkillOptions; export interface SkillMetadata { name: string; description?: string; version?: string; license?: string; author?: string; } export interface PackageSkillResult { /** * Path to packaged skill directory */ outputPath: string; /** * Skill metadata extracted from frontmatter */ skill: SkillMetadata; /** * Files included in package */ files: { root: string; dependencies: string[]; }; /** * Package artifacts generated */ artifacts?: { directory?: string; zip?: string; npm?: string; marketplace?: string; }; /** References excluded from bundle */ excludedReferences?: string[] | undefined; /** * Post-build integrity issues — issues that the override config did NOT suppress. * Empty (or omitted) means all post-build checks passed. */ postBuildIssues?: ValidationIssue[] | undefined; /** Full validation result against the built output. */ postBuildValidation?: PackagingValidationResult | undefined; /** True when any emitted issue has resolved severity 'error'. */ hasErrors: boolean; } /** * Specification for building a single skill. Used with packageSkills(). */ export interface SkillBuildSpec { /** Absolute path to the SKILL.md file */ skillPath: string; /** Packaging options for this skill */ options: PackageSkillOptions; } /** * What ONE skill in a `packageSkills` batch produced. * * A discriminated union rather than a nullable result, because a skill that * threw produced no output path, no metadata and no file list — a synthetic * `PackageSkillResult` for it would have to invent all three, and every * consumer reading `files.dependencies.length` would then report a file count * for a bundle that does not exist on disk. */ export type SkillPackageOutcome = { status: 'built'; skillPath: string; result: PackageSkillResult; } | { status: 'failed'; skillPath: string; error: Error; }; /** * Package multiple skills with a shared ResourceRegistry. * * Creates one registry for the entire project (crawling all .md files once), * then packages each skill against the shared registry. This eliminates * redundant I/O when building multiple skills from the same project. * * **One skill's failure never discards the batch.** `packageSkill` reports most * problems by RETURNING a result whose `hasErrors` is set, which callers already * degrade gracefully on — but it also THROWS on structural packaging failures * (an absent or unreadable `files:` source). Letting that throw escape the loop * made the two failure paths behave in opposite ways through one contract: * measured on a 90-skill project, one such failure discarded 89 completed builds * and collapsed the whole report into a single string. Each iteration is * therefore contained and reported as a `failed` outcome instead. * * A filename collision is NOT one of the throwing paths — it is a returned * `FILENAME_COLLISION` finding. Do not reach for a collision as the fixture when * testing this containment: the loop completes normally either way, so such a * test passes whether or not the containment exists. * * The registry build is deliberately OUTSIDE the containment: it is the run's * shared prerequisite, so its failure really does doom every skill and must * still propagate. Only per-skill work is contained. * * @param skills - Array of skill build specifications * @param projectRoot - Absolute path to the project root directory * @param allowLedger - The RUN's allow-usage ledger. Required, not * optional-with-a-default, for the same reason `runValidationFramework`'s is: * this function loops, so it can never honestly conclude on its own that an * allow entry matched nothing — an entry matched while building skill A is * USED for the run. It is never drained here; the caller drains it once with * `allowUnusedIssues()` after everything in the invocation has been seen * (`vat build` also validates the SOURCE tree, whose matches count too). * Containment does not change that: a skill that threw may still have matched * allow entries before it threw, and those matches count for the run. * @returns One outcome per input spec, in input order * * @example * ```typescript * const specs: SkillBuildSpec[] = [ * { skillPath: '/project/skills/SKILL.md', options: { outputPath: '/out/skill-a' } }, * { skillPath: '/project/skills/SKILL2.md', options: { outputPath: '/out/skill-b' } }, * ]; * const ledger = createAllowUsageLedger(); * const outcomes = await packageSkills(specs, '/project', ledger); * const runIssues = allowUnusedIssues(ledger); * ``` */ export declare function packageSkills(skills: SkillBuildSpec[], projectRoot: string, allowLedger: AllowUsageLedger): Promise; /** * Package a skill with all its dependencies * * This is the unified packaging logic used by all flows. * Works with any SKILL.md file, whether generated or handwritten. * * @param skillPath - Absolute path to SKILL.md file * @param options - Packaging options * @returns Package result with metadata and artifact paths * * @example * ```typescript * const result = await packageSkill( * 'vat-example-cat-agents/resources/skills/SKILL.md', * { formats: ['directory', 'zip'] } * ); * ``` */ export declare function packageSkill(skillPath: string, options?: PackageSkillOptions): Promise; /** * Build THE project registry: every markdown file under `projectRoot`, parsed, * with links resolved and the project config attached. * * This is the one builder for "the registry a packaging run works against", and * every lane that packages skills must call it EXACTLY ONCE per run and pass the * result into each {@link packageSkill}. It crawls and parses the entire project * — on a large monorepo that is thousands of files and tens of seconds — so a * caller that lets `packageSkill` fall back to it per skill turns a fixed * project-sized cost into a per-skill one. * * It also carries the config, which decides collection membership: the packager * rewrites frontmatter URI-references per collection schema, mirroring the * validator. A registry built without config silently belongs to no collection, * so a lane that built its own config-less registry rewrote frontmatter * differently from the lane that used this one — which is why there is now only * one builder rather than two that happened to differ in one argument. */ export declare function createProjectRegistry(projectRoot: string): Promise; /** * Generate a synthetic resource ID for a non-markdown asset that collides with an * existing markdown resource. Uses the absolute asset path prefixed with `asset::` * to guarantee uniqueness — this id is used only for skill-packager internal * lookups (output registry + link rewriting), not for user-facing output. */ export declare function synthesizeAssetId(assetPath: string): string; /** * Determine the resource subdirectory for a file. * * For claude-web target: uses the existing references directory. * For claude-code target: uses content-type routing based on file extension. */ export declare function getResourceSubdirForFile(filePath: string, target: PackagingTarget): string; /** * Extract H1 title from markdown content * * @param content - Markdown content * @returns The H1 title text, or undefined if not found */ export declare function extractH1Title(content: string): string | undefined; /** * Find the common ancestor directory of all file paths * * @param filePaths - Array of absolute file paths * @returns Common ancestor directory path */ export declare function findCommonAncestor(filePaths: string[]): string; /** * Generate target path based on naming strategy * * @param filePath - Absolute path to the source file * @param basePath - Base path to calculate relative path from * @param strategy - Naming strategy to use * @param stripPrefix - Path prefix to remove before applying strategy (works for all strategies) * @returns Target path (relative) for the packaged resource */ export declare function generateTargetPath(filePath: string, basePath: string, strategy?: ResourceNamingStrategy, stripPrefix?: string): string; /** * Thrown when a claude-web ZIP exceeds the 8MB Claude.ai upload limit. * The CLI catches this and exits with code 1. */ export declare class ZipSizeLimitError extends Error { readonly sizeBytes: number; readonly limitBytes: number; constructor(sizeBytes: number, limitBytes: number); } //# sourceMappingURL=skill-packager.d.ts.map