vet-skills 0.4.0 → 0.5.1

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.
Files changed (2) hide show
  1. package/dist/index.js +92 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7616,7 +7616,7 @@ import { parseArgs } from "node:util";
7616
7616
  import { spawnSync } from "node:child_process";
7617
7617
  import { readFileSync as readFileSync3, existsSync as existsSync4 } from "node:fs";
7618
7618
  import { resolve, join as join4, dirname, basename } from "node:path";
7619
- import { hostname, platform, userInfo } from "node:os";
7619
+ import { hostname, platform as platform2, userInfo } from "node:os";
7620
7620
  import { createHash } from "node:crypto";
7621
7621
  import { createInterface } from "node:readline";
7622
7622
 
@@ -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.4.0";
7632
+ var VERSION = "0.5.1";
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;
@@ -7735,6 +7735,33 @@ function stateSummary() {
7735
7735
  const sz = (f) => existsSync(join(HOME, f)) ? statSync(join(HOME, f)).size : 0;
7736
7736
  return { home: HOME, cacheEntries: cacheSize(), cacheBytes: sz("cache.json"), logBytes: sz("log.jsonl") };
7737
7737
  }
7738
+ function telemetryEnabled() {
7739
+ const env = (process.env.VET_SKILLS_TELEMETRY ?? "").toLowerCase();
7740
+ if (["0", "false", "off", "no"].includes(env)) return false;
7741
+ if (process.env.DO_NOT_TRACK === "1") return false;
7742
+ const p = join(HOME, "telemetry");
7743
+ if (existsSync(p)) return readFileSync(p, "utf8").trim() !== "off";
7744
+ return true;
7745
+ }
7746
+ function setTelemetry(on2) {
7747
+ ensureHome();
7748
+ writeFileSync(join(HOME, "telemetry"), on2 ? "on\n" : "off\n", { mode: 384 });
7749
+ }
7750
+ function telemetryNoticePending() {
7751
+ ensureHome();
7752
+ const p = join(HOME, "telemetry-notice");
7753
+ if (existsSync(p)) return false;
7754
+ writeFileSync(p, (/* @__PURE__ */ new Date()).toISOString() + "\n", { mode: 384 });
7755
+ return true;
7756
+ }
7757
+ function hintOncePerDay(key) {
7758
+ ensureHome();
7759
+ const p = join(HOME, `hint-${key.replace(/[^\w-]/g, "_")}`);
7760
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7761
+ if (existsSync(p) && readFileSync(p, "utf8").trim() === today) return false;
7762
+ writeFileSync(p, today + "\n", { mode: 384 });
7763
+ return true;
7764
+ }
7738
7765
 
7739
7766
  // src/api.ts
7740
7767
  async function call(path, init = {}, timeoutMs = 8e3) {
@@ -7792,6 +7819,12 @@ function detonate(tgz, opts = {}) {
7792
7819
  const q = new URLSearchParams();
7793
7820
  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);
7794
7821
  }
7822
+ function sendTelemetry(body) {
7823
+ return call("/api/v1/telemetry", { method: "POST", body: JSON.stringify(body) }, 1500);
7824
+ }
7825
+
7826
+ // src/check.ts
7827
+ import { platform } from "node:os";
7795
7828
 
7796
7829
  // src/render.ts
7797
7830
  var on = !!process.stdout.isTTY && !process.env.NO_COLOR;
@@ -7859,7 +7892,7 @@ async function check(q, opts = {}) {
7859
7892
  let source = "api";
7860
7893
  let offline = false;
7861
7894
  let cacheAge;
7862
- let res = await resolveSkill(q);
7895
+ let res = await resolveSkill(q, opts.registry);
7863
7896
  if (!res.ok && !res.offline && org) {
7864
7897
  const bare = q.replace(/^[^/]+\//, "");
7865
7898
  const priv = await resolveSkill(`${org.slug}/${bare}`, "private");
@@ -7908,6 +7941,10 @@ async function check(q, opts = {}) {
7908
7941
  offline,
7909
7942
  requestId: remote?.approvalRequestId
7910
7943
  });
7944
+ if (!org && telemetryEnabled()) {
7945
+ if (telemetryNoticePending()) process.stderr.write(c.muted("vet-skills sends anonymous usage (skill name, version, decision, agent type; never code, prompts or paths). Opt out: vet-skills telemetry off") + "\n");
7946
+ await sendTelemetry({ skill: resolved?.skill.name ?? q, semver: resolved?.version.semver ?? null, registry: resolved?.skill.registry ?? null, decision: decision.decision, would_be: decision.wouldBe ?? null, agent_kind: opts.agentKind ?? "other", hook_version: VERSION, os: platform(), offline }).catch(() => null);
7947
+ }
7911
7948
  return { resolved, decision, source, cacheAge, remote, offline, policyLabel };
7912
7949
  }
7913
7950
  function renderCheck(r) {
@@ -8071,10 +8108,41 @@ function extractRemoteMcp(command) {
8071
8108
  const name = positional.find((a) => a !== url) ?? new URL(url).hostname;
8072
8109
  return { name, url };
8073
8110
  }
8111
+ function extractStdioPackage(command) {
8112
+ const tail = command.split(/\s--\s/)[1];
8113
+ if (!tail) return null;
8114
+ const args = (tail.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map((a) => a.replace(/^(['"])(.*)\1$/, "$2"));
8115
+ const [bin, ...rest2] = args;
8116
+ const strip = (s) => s.replace(/(.)@[^@/]+$/, "$1");
8117
+ if (/^(npx|bunx|pnpm)$/.test(bin ?? "")) {
8118
+ const pkg = rest2.find((a) => !a.startsWith("-") && a !== "dlx" && a !== "exec");
8119
+ return pkg ? { q: strip(pkg).replace(/^@/, ""), registry: "npm" } : null;
8120
+ }
8121
+ if (/^(uvx|pipx)$/.test(bin ?? "")) {
8122
+ const pkg = rest2.find((a) => !a.startsWith("-") && a !== "run");
8123
+ return pkg ? { q: strip(pkg).toLowerCase().replace(/\[.*\]$/, ""), registry: "pypi" } : null;
8124
+ }
8125
+ if (bin === "docker") {
8126
+ const img = rest2.find((a) => /^(ghcr\.io\/|docker\.io\/)?[\w.-]+\/[\w.-]+(:[\w.-]+)?$/.test(a) && !a.startsWith("-"));
8127
+ if (!img) return null;
8128
+ const m = img.replace(/:[\w.-]+$/, "").match(/mcp\/([\w.-]+)$/);
8129
+ return m ? { q: m[1], registry: "docker" } : null;
8130
+ }
8131
+ return null;
8132
+ }
8074
8133
  function extractInstall(command) {
8075
8134
  for (const p of INSTALL_PATTERNS) {
8076
8135
  const m = command.match(p.re);
8077
- if (m) return { q: m[1].replace(/^['"]|['"]$/g, ""), kind: p.kind };
8136
+ if (!m) continue;
8137
+ const q = m[1].replace(/^['"]|['"]$/g, "");
8138
+ if (p.kind === "mcp") {
8139
+ const pkg = extractStdioPackage(command);
8140
+ if (pkg) return { q: pkg.q, kind: "mcp", registry: pkg.registry };
8141
+ const remote = extractRemoteMcp(command);
8142
+ if (remote) return { q: remote.name, kind: "mcp" };
8143
+ }
8144
+ if (p.kind === "plugin") return { q: q.split("@")[0], kind: "plugin", registry: "plugins" };
8145
+ return { q, kind: p.kind };
8078
8146
  }
8079
8147
  return null;
8080
8148
  }
@@ -8090,7 +8158,7 @@ async function hookEval(stdin) {
8090
8158
  if (/^\s*vet-skills\s+(check|why|status|sync|policy|detonate|hook|join|leave|block|publish)\b/.test(cmd2)) return 0;
8091
8159
  const found = extractInstall(cmd2);
8092
8160
  if (!found) return 0;
8093
- const r = await check(found.q, { quiet: true, agentKind: "claude_code" });
8161
+ const r = await check(found.q, { quiet: true, agentKind: "claude_code", registry: found.registry });
8094
8162
  const out = renderOutcome(r);
8095
8163
  const remote = found.kind === "mcp" ? extractRemoteMcp(cmd2) : null;
8096
8164
  const watch = async () => {
@@ -8122,10 +8190,11 @@ ${out}
8122
8190
  `);
8123
8191
  return 2;
8124
8192
  case "block":
8125
- process.stderr.write(`vet-skills blocked this install by org policy.
8193
+ process.stderr.write(`vet-skills blocked this install by ${readOrg() ? "org" : "your"} policy.
8126
8194
  ${renderCheck(r)}
8127
8195
  ${out}
8128
8196
  `);
8197
+ if (!readOrg() && hintOncePerDay("teams")) process.stderr.write("Running this for a team? One policy across every developer's agents, approvals and inventory: https://killerskills.ai/pricing\n");
8129
8198
  return 2;
8130
8199
  }
8131
8200
  return 0;
@@ -8180,6 +8249,11 @@ async function ask(question, choices) {
8180
8249
  }
8181
8250
  var USAGE = `vet-skills ${VERSION} \u2014 check agent skills and MCP servers before they run.
8182
8251
 
8252
+ Get started:
8253
+ vet-skills hook install claude-code check every install Claude Code makes (one time)
8254
+ vet-skills check <skill> see the verdict for anything before installing it
8255
+ vet-skills join <org> --token <t> enroll this machine in your team's policy (token from the dashboard)
8256
+
8183
8257
  vet-skills check <skill> dry-run the decision
8184
8258
  vet-skills install <skill> check, then \`npx skills add\`
8185
8259
  vet-skills detonate <path> [--publisher h] upload a local bundle to the sandbox and print its verdict
@@ -8214,7 +8288,7 @@ async function main() {
8214
8288
  usage();
8215
8289
  return 1;
8216
8290
  }
8217
- const r = await check(rest[0], { agentKind: values.agent });
8291
+ const r = await check(rest[0], { agentKind: values.agent, registry: values.registry });
8218
8292
  if (values.json) {
8219
8293
  console.log(JSON.stringify({ decision: r.decision, resolved: r.resolved, source: r.source, offline: r.offline }, null, 2));
8220
8294
  return r.decision.decision === "block" ? 2 : 0;
@@ -8365,7 +8439,7 @@ async function main() {
8365
8439
  machine_label: hostname(),
8366
8440
  agent_kind: values.agent ?? "other",
8367
8441
  hook_version: VERSION,
8368
- os: platform(),
8442
+ os: platform2(),
8369
8443
  installed: [],
8370
8444
  ci: values.ci === true
8371
8445
  });
@@ -8432,6 +8506,16 @@ async function main() {
8432
8506
  console.log(`${c.verified("\u2713")} ${handle} added to the personal block list${values.reason ? ` (${values.reason})` : ""}.`);
8433
8507
  return 0;
8434
8508
  }
8509
+ case "telemetry": {
8510
+ const sub = rest[0] ?? "status";
8511
+ if (sub === "on" || sub === "off") {
8512
+ setTelemetry(sub === "on");
8513
+ console.log(`${c.verified("\u2713")} telemetry ${sub}`);
8514
+ return 0;
8515
+ }
8516
+ console.log(`telemetry ${telemetryEnabled() ? "on" : "off"} ${c.muted(readOrg() ? "(enrolled: decisions report to the org, not here)" : "(anonymous: skill, version, decision, agent type; never code, prompts or paths)")}`);
8517
+ return 0;
8518
+ }
8435
8519
  case "status": {
8436
8520
  const org = readOrg();
8437
8521
  const { policy, source } = readPolicy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-skills",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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": {