vet-skills 0.1.0 → 0.3.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/README.md CHANGED
@@ -4,9 +4,10 @@ Check agent skills and MCP servers before they run. Verdicts come from
4
4
  [KillerSkills](https://killerskills.ai); the policy is yours or your org's.
5
5
 
6
6
  ```
7
- npx skills add killerskills/vet-skills # soon: the meta-skill
8
- npm i -g vet-skills # the CLI
7
+ npm i -g vet-skills # the CLI (Node 20+)
9
8
  vet-skills hook install claude-code # intercept every install in Claude Code
9
+ vet-skills check <skill> # or just ask before you add one
10
+ vet-skills publish ./my-skill # org members: detonate + list in the private registry
10
11
  ```
11
12
 
12
13
  ## What it does
package/dist/index.js CHANGED
@@ -7629,7 +7629,7 @@ import { mkdirSync, readFileSync, writeFileSync, existsSync, appendFileSync, chm
7629
7629
  import { homedir } from "node:os";
7630
7630
  import { join } from "node:path";
7631
7631
  import { randomUUID } from "node:crypto";
7632
- var VERSION = "0.1.0";
7632
+ var VERSION = "0.3.0";
7633
7633
  var HOME = process.env.VET_SKILLS_HOME ?? join(homedir(), ".vet-skills");
7634
7634
  var API = (process.env.KILLERSKILLS_API ?? "https://killerskills.ai").replace(/\/$/, "");
7635
7635
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -7759,7 +7759,7 @@ async function call(path, init = {}, timeoutMs = 8e3) {
7759
7759
  function resolveSkill(q, registry) {
7760
7760
  const qs = new URLSearchParams({ q });
7761
7761
  if (registry) qs.set("registry", registry);
7762
- return call(`/api/v1/resolve?${qs}`);
7762
+ return call(`/api/v1/resolve?${qs}`, { headers: orgHeaders() });
7763
7763
  }
7764
7764
  function orgHeaders() {
7765
7765
  const token = readToken();
@@ -7778,8 +7778,13 @@ function report(body) {
7778
7778
  if (!org) return Promise.resolve({ ok: false, error: "not joined", offline: false });
7779
7779
  return call(`/api/v1/orgs/${org.slug}/agents/${org.agentId}/report`, { method: "POST", body: JSON.stringify(body), headers: orgHeaders() }, 8e3);
7780
7780
  }
7781
+ function publish(tgz) {
7782
+ const org = readOrg();
7783
+ if (!org) return Promise.resolve({ ok: false, status: 0, error: "not enrolled in an organization (vet-skills join <org> --token \u2026)", offline: false });
7784
+ return call(`/api/v1/orgs/${org.slug}/agents/${org.agentId}/publish`, { method: "POST", body: new Blob([tgz]), headers: { "content-type": "application/gzip", ...orgHeaders() } }, 18e4);
7785
+ }
7781
7786
  function detonate(tgz, opts = {}) {
7782
- const q = new URLSearchParams({ registry: opts.registry ?? "private" });
7787
+ const q = new URLSearchParams();
7783
7788
  return call(`/api/v1/detonate?${q}`, { method: "POST", body: new Blob([tgz]), headers: { "content-type": "application/gzip", ...opts.publisher ? { "x-vs-publisher": opts.publisher } : {} } }, 18e4);
7784
7789
  }
7785
7790
 
@@ -7849,7 +7854,12 @@ async function check(q, opts = {}) {
7849
7854
  let source = "api";
7850
7855
  let offline = false;
7851
7856
  let cacheAge;
7852
- const res = await resolveSkill(q);
7857
+ let res = await resolveSkill(q);
7858
+ if (!res.ok && !res.offline && org) {
7859
+ const bare = q.replace(/^[^/]+\//, "");
7860
+ const priv = await resolveSkill(`${org.slug}/${bare}`, "private");
7861
+ if (priv.ok) res = priv;
7862
+ }
7853
7863
  if (res.ok) {
7854
7864
  resolved = res.data;
7855
7865
  const cached = cacheGet(resolved.version.artifact_hash);
@@ -7985,6 +7995,27 @@ var INSTALL_PATTERNS = [
7985
7995
  { re: /claude\s+mcp\s+add(?:-json)?\s+(\S+)/, kind: "mcp" },
7986
7996
  { re: /vet-skills\s+install\s+(\S+)/, kind: "skill" }
7987
7997
  ];
7998
+ function extractRemoteMcp(command) {
7999
+ const m = command.match(/claude\s+mcp\s+add(?:-json)?\s+(.*)$/s);
8000
+ if (!m) return null;
8001
+ const args = (m[1].match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map((a) => a.replace(/^(['"])(.*)\1$/, "$2"));
8002
+ const takesValue = /^(--transport|-t|--scope|-s|--header|-H|--env|-e)$/;
8003
+ const positional = [];
8004
+ for (let i = 0; i < args.length; i++) {
8005
+ const a = args[i];
8006
+ if (a === "--") break;
8007
+ if (takesValue.test(a)) {
8008
+ i += 1;
8009
+ continue;
8010
+ }
8011
+ if (a.startsWith("-")) continue;
8012
+ positional.push(a);
8013
+ }
8014
+ const url = positional.find((a) => /^https?:\/\//.test(a));
8015
+ if (!url) return null;
8016
+ const name = positional.find((a) => a !== url) ?? new URL(url).hostname;
8017
+ return { name, url };
8018
+ }
7988
8019
  function extractInstall(command) {
7989
8020
  for (const p of INSTALL_PATTERNS) {
7990
8021
  const m = command.match(p.re);
@@ -8006,12 +8037,18 @@ async function hookEval(stdin) {
8006
8037
  if (!found) return 0;
8007
8038
  const r = await check(found.q, { quiet: true, agentKind: "claude_code" });
8008
8039
  const out = renderOutcome(r);
8040
+ const remote = found.kind === "mcp" ? extractRemoteMcp(cmd2) : null;
8041
+ const watch = async () => {
8042
+ if (remote && readOrg()) await report({ installed: [], removed: [], mcp_servers: [remote] }).catch(() => null);
8043
+ };
8009
8044
  switch (r.decision.decision) {
8010
8045
  case "allow":
8011
8046
  case "observe":
8047
+ await watch();
8012
8048
  process.stderr.write(out + "\n");
8013
8049
  return 0;
8014
8050
  case "sandbox":
8051
+ await watch();
8015
8052
  process.stderr.write(out + "\n");
8016
8053
  return 0;
8017
8054
  case "approval_required":
@@ -8087,6 +8124,7 @@ var USAGE = `vet-skills ${VERSION} \u2014 check agent skills and MCP servers bef
8087
8124
  vet-skills check <skill> dry-run the decision
8088
8125
  vet-skills install <skill> check, then \`npx skills add\`
8089
8126
  vet-skills detonate <path> [--publisher h] upload a local bundle to the sandbox and print its verdict
8127
+ vet-skills publish <path> detonate and publish to your organization's private registry
8090
8128
  vet-skills why <skill> last decision and the rule behind it
8091
8129
  vet-skills join <org> --token <t> [--email you@co] enroll this machine in an org
8092
8130
  vet-skills leave back to the personal policy
@@ -8184,7 +8222,7 @@ async function main() {
8184
8222
  }
8185
8223
  process.stderr.write(`${c.muted("Detonating")} ${basename(dir)} ${c.muted(`(${(tar.stdout.length / 1024).toFixed(0)} KB)`)} \u2026
8186
8224
  `);
8187
- const res = await detonate(new Uint8Array(tar.stdout), { publisher: values.publisher, registry: values.registry });
8225
+ const res = await detonate(new Uint8Array(tar.stdout), { publisher: values.publisher });
8188
8226
  if (!res.ok) {
8189
8227
  console.log(`${c.caution("!")} Detonation failed (${res.status ?? "offline"}): ${res.error}`);
8190
8228
  return 1;
@@ -8205,6 +8243,42 @@ async function main() {
8205
8243
  console.log(c.muted(`${API}${d.skill.page}`));
8206
8244
  return d.verdict.label === "blocked" ? 2 : 0;
8207
8245
  }
8246
+ case "publish": {
8247
+ if (!rest[0]) {
8248
+ usage();
8249
+ return 1;
8250
+ }
8251
+ const org = readOrg();
8252
+ if (!org) {
8253
+ console.log(`${c.blocked("\u2717")} not enrolled in an organization. Run ${c.bold("vet-skills join <org> --token \u2026")} first.`);
8254
+ return 1;
8255
+ }
8256
+ const dir = resolve(rest[0]);
8257
+ if (!existsSync3(join3(dir, "SKILL.md"))) {
8258
+ console.log(`${c.blocked("\u2717")} ${dir} has no SKILL.md (name and version come from its frontmatter)`);
8259
+ return 1;
8260
+ }
8261
+ const tar = spawnSync("tar", ["--exclude", "node_modules", "--exclude", ".git", "-czf", "-", "-C", dirname(dir), basename(dir)], { maxBuffer: 64 * 1024 * 1024 });
8262
+ if (tar.status !== 0) {
8263
+ console.log(`${c.blocked("\u2717")} tar failed: ${tar.stderr.toString().trim()}`);
8264
+ return 1;
8265
+ }
8266
+ process.stderr.write(`${c.muted("Publishing")} ${basename(dir)} ${c.muted(`to ${org.slug} (${(tar.stdout.length / 1024).toFixed(0)} KB) \u2014 detonating first \u2026`)}
8267
+ `);
8268
+ const res = await publish(new Uint8Array(tar.stdout));
8269
+ if (!res.ok) {
8270
+ console.log(`${c.caution("!")} Publish failed (${res.status ?? "offline"}): ${res.error}`);
8271
+ return 1;
8272
+ }
8273
+ const d = res.data;
8274
+ const paint = d.verdict.label === "verified" ? c.verified : d.verdict.label === "caution" ? c.caution : c.blocked;
8275
+ console.log(`${paint(d.verdict.label.toUpperCase())} ${c.bold(String(d.verdict.score))} ${c.muted(`\xB7 ${d.skill.publisher}/${d.skill.name}@${d.skill.semver} \xB7 ${d.skill.created ? "published" : "re-detonated (same bytes already published)"} \xB7 ${d.verdict.duration_ms} ms`)}`);
8276
+ for (const f of d.findings.slice(0, 6)) console.log(` ${c.muted(f.rule_id)} ${f.severity.padEnd(8)} ${f.title}`);
8277
+ if (d.findings.length > 6) console.log(c.muted(` \u2026 ${d.findings.length - 6} more`));
8278
+ console.log(c.muted(`${API}/app/${org.slug}/registry \xB7 ${API}${d.skill.page}`));
8279
+ console.log(c.muted(`Teammates install it with: vet-skills install ${org.slug}/${d.skill.name}`));
8280
+ return d.verdict.label === "blocked" ? 2 : 0;
8281
+ }
8208
8282
  case "why": {
8209
8283
  const log = readLog().filter((e) => !rest[0] || e.skill === rest[0] || `${e.skill}@${e.semver}` === rest[0]);
8210
8284
  const last = log.at(-1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-skills",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Vet agent skills and MCP servers before they run. Verdicts from KillerSkills, policy from your org.",
5
5
  "type": "module",
6
6
  "bin": {