🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

skill-kits

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

skill-kits

Scaffold and build Agent Skills in TypeScript — compile to a single zero-dependency ESM file

latest
Source
npmnpm
Version
1.1.1
Version published
Maintainers
1
Created
Source

skill-kits

English | 中文

Skill engineering — author Agent Skills in TypeScript, compile to a single-file ESM, run with zero dependencies.
Output follows the agentskills.io specification and requires Node.js >= 18.

Why skill-kits?

Pain pointSolution
No reuse of common modulesskill-kits/runtime ships HTTP, routing, output, and error utilities you can import directly; the monorepo from init includes packages/shared, which esbuild inlines into the output
No standard templatepnpm new <name> generates a spec-aligned SKILL.md + project skeleton
JS lacks type hints, TS depends on tsc/bunAuthor Skills in TS with full type hints; pnpm build uses esbuild to bundle into a single main.mjs in seconds — zero-dependency output that runs on Node alone on the Agent side
Lack of quality checkspnpm lint validates SKILL.md for name/dir consistency, line count, reference validity, and description triggerability; build runs lint first by default

Install

npx skill-kits init my-skills   # or in the current directory: npx skill-kits init
cd my-skills && pnpm install

init generates a pnpm monorepo:

my-skills/
├── pnpm-workspace.yaml
├── package.json
└── packages/
    ├── shared/        # shared business modules across Skills (@skills/shared)
    └── skills/        # one sub-package per Skill, generated by new

Quick start

pnpm new daily-report              # add a Skill
pnpm dev daily-report --out ~/.agent/skills   # watch + sync to the agent directory
pnpm build daily-report            # build (auto lint + zip)
pnpm test daily-report             # run unit tests

Write TS, run JS

      source                                output
┌──────────────────┐           ┌──────────────────────────┐
│  src/main.ts     │           │  dist/                   │
│  src/commands/   │  build    │  ├── <skill-name>/       │
│  references/     │ ───────►  │  │   ├── SKILL.md        │
│  assets/         │  esbuild  │  │   ├── scripts/        │
│  SKILL.md        │           │  │   │   └── main.mjs    │
└──────────────────┘           │  │   ├── references/     │
                               │  │   └── assets/         │
                               │  └── <skill-name>.zip    │
                               └──────────────────────────┘
  TypeScript + runtime import      single-file ESM, zero deps
                                   Agent: node scripts/main.mjs

CLI

Inside the workspace generated by init, the pnpm new / dev / build / lint scripts are already wired up; the forms below are equivalent.

CommandPurpose
npx skill-kits init [dir]Generate a Skill workspace skeleton (defaults to the current dir)
pnpm new <name>Add a Skill (with references/ assets/ placeholders)
pnpm build [name]Build → lint + pack into dist/<name>.zip by default
pnpm dev <name>Watch mode; src/ + SKILL.md + references/ + assets/ sync instantly
pnpm lint [name]Lint SKILL.md only
pnpm test [name]Run Skill unit tests (src/**/*.test.ts, based on node:test + tsx)
pnpm pack <name>Pack dist/<name>/ into dist/<name>.zip (build does this by default)

Options

CommandOptionDescription
init-n, --name <name>Project name; defaults to the target directory name
--locale <locale>UI / template language: en | zh-CN (defaults to env detection)
new--cwd <dir>Workspace root directory (defaults to the current directory)
--locale <locale>Template language; defaults to the workspace config, then env
build--minifyMinify the output JS
--no-lintSkip lint before building
--no-packSkip packing into a zip after building
dev--out <dir>Output to <dir>/<name>/, often used to sync directly to an agent skills dir
--no-sourcemapDisable sourcemap (inline by default)
--run [args]After rebuild, auto-run node <bundle> [args] for quick local regression
lint--strictTreat warnings as errors
test--watchWatch files and rerun tests automatically
pack--cwd <dir>Workspace root directory (defaults to the current directory)
all--cwd <dir>Workspace root directory (defaults to the current directory)

Runtime API

Import from skill-kits/runtime; everything is inlined into the output at build time.

Command routing

createRouter automatically handles --help, required-argument validation, and error normalization:

import { createRouter, writeResult } from "skill-kits/runtime";

const router = createRouter({
  name: "daily-report",
  description: "...",
  commonArgs: {
    // common args automatically injected into every subcommand
    domain: { type: "string", required: true, desc: "platform domain" },
    token: { type: "string", required: true, desc: "SSO token" },
  },
});

router.command({
  name: "fetch",
  description: "fetch yesterday's data",
  args: {
    date: { type: "string", required: true, desc: "YYYY-MM-DD" },
    limit: { type: "number", default: 100, desc: "max items" },
    tag: { type: "list", desc: "filter tags, repeatable" },
    env: { type: "string", choices: ["boe", "online"] as const, desc: "environment" },
    filter: { type: "json", desc: "complex filter, parsed from JSON" },
  },
  async handler({ date, limit, tag, env, filter, domain, token }) {
    // env is typed as "boe" | "online" (inferred from choices); filter is already JSON.parse-d
    writeResult({ ok: true, items: [] });
  },
});

router.run(process.argv.slice(2));

args / commonArgs support 5 types: string / number / boolean / list / json. A missing required arg throws UserInputError; choices restricts the enum values and provides type inference; type: "json" auto-runs JSON.parse.

Output

writeResult(payload)                                  // stdout, single-line JSON for the Agent to consume
writeError(errorOrMessage, { code?, extra? })         // stderr + exitCode=1
notify(message)                                       // stderr, progress/stage hints (ℹ️ prefix)
handleCliError(error)                                 // top-level catch fallback, auto-detects SkillError subclasses

HTTP

Zero dependencies, built on the global fetch (Node 18+). It never throws — network errors and non-2xx responses are both expressed via res.ok, and the caller decides how to handle them:

import { httpGet, httpPost } from "skill-kits/runtime";

const res = await httpGet<UserInfo>("https://api.example.com/me", {
  headers: { authorization: `Bearer ${token}` },
  query: { fields: "id,name" },
  timeoutMs: 10_000,
});
if (!res.ok) throw new HttpError(res.status, url, res.statusText);
console.log(res.data?.name);

HttpRequestOptions: headers / query / body / signal / timeoutMs / redirect.

For PUT / DELETE and other methods, use httpRequest directly — see node_modules/skill-kits/dist/runtime/http.d.ts for the full parameter list.

Environment variables

Helpers for reading environment variables. requireEnv throws a unified USER_INPUT_ERROR (code=USER_INPUT_ERROR, details includes { env: "VAR_NAME" }) when missing, so the LLM can guide the user to configure it:

import { requireEnv, optionalEnv } from "skill-kits/runtime";

const token = requireEnv("OPENAPI_TOKEN", {
  hint: "Apply at: https://example.com/get-token",
});
// unset → stderr: { "ok": false, "code": "USER_INPUT_ERROR", "error": "Missing environment variable OPENAPI_TOKEN. Apply at: ..." }

const pat = optionalEnv("FIGMA_PERSONAL_ACCESS_TOKEN"); // string | null

Polling heartbeat

For long-polling scenarios (D2C code generation, SSO callbacks, etc.), use sleepWithHeartbeat instead of setTimeout to write progress to stderr every 5 seconds, preventing the Agent from mistakenly killing an idle process:

import { sleepWithHeartbeat } from "skill-kits/runtime";

await sleepWithHeartbeat(60_000, {
  message: (rem) => `Waiting for code generation... ${rem}s left`,
});
// stderr every 5s: ℹ️ Waiting for code generation... 55s left
// stderr every 5s: ℹ️ Waiting for code generation... 50s left
// ...

SleepWithHeartbeatOptions: intervalMs / message (string or function).

Error system

All business errors extend SkillError; handleCliError automatically detects subclasses and outputs structured JSON to stderr. Built-in error codes let the LLM match an appropriate handling strategy:

ClasscodeScenario
UserInputErrorUSER_INPUT_ERRORMissing argument / bad format
AuthErrorAUTH_ERRORToken expired / insufficient perms
HttpErrorHTTP_ERRORUpstream HTTP non-2xx
BusinessApiErrorBIZ_<code>HTTP 200 but business code ≠ 0
import {
  SkillError,
  UserInputError,
  BusinessApiError,
} from "skill-kits/runtime";

// UserInputError: argument validation failed
throw new UserInputError("activityId is required", { field: "activityId" });
// stderr → {"ok":false,"code":"USER_INPUT_ERROR","error":"activityId is required","details":{"field":"activityId"}}

// Custom BusinessApiError
throw new BusinessApiError(-10000, "token expired", {
  hintMap: { [-10000]: "Please log in again", [-14]: "Record not found" },
});
// stderr → {"ok":false,"code":"BIZ_-10000","error":"[code=-10000] token expired (Please log in again)"}

// Custom business error: extend SkillError, name the code freely
class RateLimitError extends SkillError {
  constructor(retryAfterSec?: number) {
    super("RATE_LIMIT", "too many requests", { retryAfterSec });
  }
}
throw new RateLimitError(30);
// stderr → {"ok":false,"code":"RATE_LIMIT","error":"too many requests","details":{"retryAfterSec":30}}

Testing

Skill unit tests follow the convention packages/skills/<name>/src/**/*.test.ts, run via pnpm test [name] (built on node:test + tsx, no extra config). The usual goal is to test a command's exit behavior — call the command function, then assert the stdout JSON and exitCode. skill-kits/testing provides two helpers for that; mockFetch is only needed when the command hits the network, while pure logic can be asserted directly:

import { test } from "node:test";
import assert from "node:assert/strict";
import { mockFetch, captureOutput } from "skill-kits/testing";
import { toSeconds } from "./utils.js";
import { createActivity } from "./create-activity.js";

const ctx = { domain: "https://example.com", token: "t" }; // resolved commonArgs

// 1) Pure function — no helper needed.
test("toSeconds normalizes ms to seconds", () => {
  assert.equal(toSeconds(1717000000000), 1717000000);
});

// 2) Success path — captureOutput wraps the handler; mockFetch fakes HTTP.
test("create returns ok with backend data", async () => {
  const mock = mockFetch([
    { match: /\/activity\/create/, json: { code: 0, data: { activity_id: 9001 } } },
  ]);
  try {
    const { json, exitCode } = await captureOutput(() =>
      createActivity(ctx, { act_name: "test" }),
    );
    assert.equal(exitCode, 0);
    assert.equal((json as { activity_id: number }).activity_id, 9001);
  } finally {
    mock.restore();
  }
});

// 3) Error path — command functions throw SkillError (the router maps it to
//    exit 1 + stderr JSON at the top level), so assert on the thrown error.
test("missing required arg throws", async () => {
  await assert.rejects(
    () => captureOutput(() => createActivity(ctx, {})),
    "required",
  );
});
  • captureOutput(fn) — captures what writeResult / writeError / notify write plus process.exitCode, returning { stdout, stderr, json, exitCode } (json is the parsed stdout). Restores everything even if fn throws. Use it for the success path; for the error path, command functions throw, so reach for assert.rejects.
  • mockFetch(routes) — replaces the global fetch; match by substring / RegExp / function, reply with json / text / status / a custom response. An unmatched request throws, so missing mocks never pass silently. Call .restore() in a finally.
  • Not every test needs mockFetch — pure functions just import and assert directly.

Shared business modules

runtime handles infrastructure reuse; domain-specific utilities/constants/clients live in packages/shared:

# add the dependency in the Skill sub-package's package.json
# "dependencies": { "@skills/shared": "workspace:*" }
pnpm install
// Export your helpers/constants/clients from packages/shared/src/index.ts,
// then import them in any Skill:
// import { yourHelper } from "@skills/shared";

At build time, esbuild inlines @skills/shared into the output, keeping it a zero-dependency single file. Leave the directory empty if you don't need it.

Progressive disclosure

Put long docs in references/ and assets in assets/; the Agent references them on demand from SKILL.md to avoid polluting the context with a one-time full load. See the agentskills.io specification for reference conventions.

Language

skill-kits is English-first; CLI output and generated templates follow the resolved locale (en or zh-CN). The locale is resolved in this order:

  • --locale <locale> flag (init / new)
  • SKILL_KITS_LOCALE environment variable
  • locale field in the workspace .skillkitrc.json (used by new)
  • LC_ALL / LANG environment variables
  • fallback to en

init writes the resolved locale into .skillkitrc.json, and new inherits it so a workspace stays consistent. Templates can ship localized overrides named <base>.<locale>.<ext> (e.g. SKILL.zh-CN.md overrides SKILL.md); only the matching-locale variant is emitted, under the base name.

// .skillkitrc.json
{ "locale": "zh-CN" }

Configuration

Place a .skillkitrc.json at the workspace root to customize lint behavior:

{
  // "locale": "zh-CN",  // see the Language section above
  "lint": {
    "triggerHints": ["何时", "trigger", "use when"],
    "negativeHints": ["不要", "do not"],
    "descriptionMinChars": 80,
    "bodyLinesWarn": 400,
    "bodyLinesFail": 500
  }
}

Lint rules

RuleLevelDescription
name-matches-direrrorname must equal the parent directory name
body-line-limiterrorSKILL.md body > 500 lines
body-line-softwarnSKILL.md body > 400 lines, consider splitting into references/
ref-relativeerrorReferences must be relative paths
ref-deptherror / warn../ raises error; > 1 directory level raises warn
description-lengthwarndescription < 80 chars; too short risks under-triggering
description-triggerinfodescription lacks "when to trigger" hints
description-negativeinfoConsider adding "do not trigger" counterexamples
frontmatter-unknown-keywarnA frontmatter key not defined by the spec appeared

License

MIT

Keywords

agent

FAQs

Package last updated on 19 Jun 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts