vet-skills 0.2.0 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +115 -14
  2. package/package.json +10 -11
package/dist/index.js CHANGED
@@ -7614,8 +7614,8 @@ var init_dist = __esm({
7614
7614
  init_dist();
7615
7615
  import { parseArgs } from "node:util";
7616
7616
  import { spawnSync } from "node:child_process";
7617
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
7618
- import { resolve, join as join3, dirname, basename } from "node:path";
7617
+ import { readFileSync as readFileSync3, existsSync as existsSync4 } from "node:fs";
7618
+ import { resolve, join as join4, dirname, basename } from "node:path";
7619
7619
  import { hostname, platform, userInfo } from "node:os";
7620
7620
  import { createHash } from "node:crypto";
7621
7621
  import { createInterface } from "node:readline";
@@ -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.2.0";
7632
+ var VERSION = "0.4.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;
@@ -7778,6 +7778,11 @@ 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 confirmRemoval(removalId, body) {
7782
+ const org = readOrg();
7783
+ if (!org) return Promise.resolve({ ok: false, error: "not joined", offline: false });
7784
+ return call(`/api/v1/orgs/${org.slug}/agents/${org.agentId}/removals/${removalId}/confirm`, { method: "POST", body: JSON.stringify(body), headers: orgHeaders() });
7785
+ }
7781
7786
  function publish(tgz) {
7782
7787
  const org = readOrg();
7783
7788
  if (!org) return Promise.resolve({ ok: false, status: 0, error: "not enrolled in an organization (vet-skills join <org> --token \u2026)", offline: false });
@@ -7986,15 +7991,86 @@ function restrictionText(r) {
7986
7991
  }
7987
7992
 
7988
7993
  // src/hook.ts
7989
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
7994
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
7995
+ import { homedir as homedir3 } from "node:os";
7996
+ import { join as join3 } from "node:path";
7997
+
7998
+ // src/sync.ts
7999
+ import { existsSync as existsSync2, rmSync } from "node:fs";
7990
8000
  import { homedir as homedir2 } from "node:os";
7991
8001
  import { join as join2 } from "node:path";
8002
+ function skillDirs(name) {
8003
+ const bare = name.replace(/^.*\//, "").replace(/@.*$/, "");
8004
+ return [join2(homedir2(), ".claude", "skills", bare), join2(process.cwd(), ".claude", "skills", bare), join2(homedir2(), ".claude", "plugins", bare)];
8005
+ }
8006
+ async function applyRemovals(pending, log = () => {
8007
+ }) {
8008
+ let done = 0;
8009
+ const byRequest = /* @__PURE__ */ new Map();
8010
+ for (const r of pending) byRequest.set(r.removal_id, [...byRequest.get(r.removal_id) ?? [], r]);
8011
+ for (const [removalId, items] of byRequest) {
8012
+ const detail = [];
8013
+ let failed = false;
8014
+ for (const it of items) {
8015
+ const dirs = skillDirs(it.skill).filter((d) => existsSync2(d));
8016
+ if (!dirs.length) {
8017
+ detail.push(`${it.skill}: not on disk`);
8018
+ continue;
8019
+ }
8020
+ try {
8021
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
8022
+ detail.push(`${it.skill}: removed ${dirs.join(", ")}`);
8023
+ } catch (e) {
8024
+ failed = true;
8025
+ detail.push(`${it.skill}: ${e.message}`);
8026
+ }
8027
+ }
8028
+ const res = await confirmRemoval(removalId, { status: failed ? "failed" : "confirmed", detail: detail.join("; ") });
8029
+ if (res.ok) {
8030
+ done += items.length;
8031
+ log(`${failed ? "!" : "\u2713"} ${items.map((i) => i.skill).join(", ")} ${failed ? "removal failed" : "removed"} (${removalId})`);
8032
+ } else log(`! could not confirm removal ${removalId}: ${res.error}`);
8033
+ }
8034
+ return done;
8035
+ }
8036
+ async function sync(log = () => {
8037
+ }) {
8038
+ const org = readOrg();
8039
+ if (!org) return { ok: false, removed: 0, error: "not joined" };
8040
+ const res = await report({ installed: [], removed: [], hook_version: void 0 });
8041
+ if (!res.ok) return { ok: false, removed: 0, error: res.error };
8042
+ const removed = await applyRemovals(res.data.pending_removals, log);
8043
+ return { ok: true, removed };
8044
+ }
8045
+
8046
+ // src/hook.ts
7992
8047
  var INSTALL_PATTERNS = [
7993
8048
  { re: /(?:npx\s+(?:-y\s+)?)?skills\s+add\s+(\S+)/, kind: "skill" },
7994
8049
  { re: /claude\s+plugin\s+install\s+(\S+)/, kind: "plugin" },
7995
8050
  { re: /claude\s+mcp\s+add(?:-json)?\s+(\S+)/, kind: "mcp" },
7996
8051
  { re: /vet-skills\s+install\s+(\S+)/, kind: "skill" }
7997
8052
  ];
8053
+ function extractRemoteMcp(command) {
8054
+ const m = command.match(/claude\s+mcp\s+add(?:-json)?\s+(.*)$/s);
8055
+ if (!m) return null;
8056
+ const args = (m[1].match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map((a) => a.replace(/^(['"])(.*)\1$/, "$2"));
8057
+ const takesValue = /^(--transport|-t|--scope|-s|--header|-H|--env|-e)$/;
8058
+ const positional = [];
8059
+ for (let i = 0; i < args.length; i++) {
8060
+ const a = args[i];
8061
+ if (a === "--") break;
8062
+ if (takesValue.test(a)) {
8063
+ i += 1;
8064
+ continue;
8065
+ }
8066
+ if (a.startsWith("-")) continue;
8067
+ positional.push(a);
8068
+ }
8069
+ const url = positional.find((a) => /^https?:\/\//.test(a));
8070
+ if (!url) return null;
8071
+ const name = positional.find((a) => a !== url) ?? new URL(url).hostname;
8072
+ return { name, url };
8073
+ }
7998
8074
  function extractInstall(command) {
7999
8075
  for (const p of INSTALL_PATTERNS) {
8000
8076
  const m = command.match(p.re);
@@ -8011,17 +8087,26 @@ async function hookEval(stdin) {
8011
8087
  }
8012
8088
  if (payload.tool_name !== "Bash" || !payload.tool_input?.command) return 0;
8013
8089
  const cmd2 = payload.tool_input.command;
8014
- if (/^\s*vet-skills\s+(check|why|status|policy|detonate|hook|join|leave|block)\b/.test(cmd2)) return 0;
8090
+ if (/^\s*vet-skills\s+(check|why|status|sync|policy|detonate|hook|join|leave|block|publish)\b/.test(cmd2)) return 0;
8015
8091
  const found = extractInstall(cmd2);
8016
8092
  if (!found) return 0;
8017
8093
  const r = await check(found.q, { quiet: true, agentKind: "claude_code" });
8018
8094
  const out = renderOutcome(r);
8095
+ const remote = found.kind === "mcp" ? extractRemoteMcp(cmd2) : null;
8096
+ const watch = async () => {
8097
+ if (!readOrg()) return;
8098
+ const res = await report({ installed: [], removed: [], ...remote ? { mcp_servers: [remote] } : {} }).catch(() => null);
8099
+ if (res?.ok && res.data.pending_removals.length) await applyRemovals(res.data.pending_removals, (l) => process.stderr.write(`vet-skills \xB7 ${l}
8100
+ `)).catch(() => 0);
8101
+ };
8019
8102
  switch (r.decision.decision) {
8020
8103
  case "allow":
8021
8104
  case "observe":
8105
+ await watch();
8022
8106
  process.stderr.write(out + "\n");
8023
8107
  return 0;
8024
8108
  case "sandbox":
8109
+ await watch();
8025
8110
  process.stderr.write(out + "\n");
8026
8111
  return 0;
8027
8112
  case "approval_required":
@@ -8046,11 +8131,11 @@ ${out}
8046
8131
  return 0;
8047
8132
  }
8048
8133
  function installClaudeCodeHook() {
8049
- const dir = join2(homedir2(), ".claude");
8050
- const path = join2(dir, "settings.json");
8051
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
8134
+ const dir = join3(homedir3(), ".claude");
8135
+ const path = join3(dir, "settings.json");
8136
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
8052
8137
  let settings = {};
8053
- if (existsSync2(path)) {
8138
+ if (existsSync3(path)) {
8054
8139
  try {
8055
8140
  settings = JSON.parse(readFileSync2(path, "utf8"));
8056
8141
  } catch {
@@ -8075,6 +8160,7 @@ var { values, positionals } = parseArgs({
8075
8160
  org: { type: "string" },
8076
8161
  yes: { type: "boolean", short: "y" },
8077
8162
  json: { type: "boolean" },
8163
+ ci: { type: "boolean" },
8078
8164
  email: { type: "string" },
8079
8165
  help: { type: "boolean", short: "h" },
8080
8166
  version: { type: "boolean", short: "v" },
@@ -8099,11 +8185,12 @@ var USAGE = `vet-skills ${VERSION} \u2014 check agent skills and MCP servers bef
8099
8185
  vet-skills detonate <path> [--publisher h] upload a local bundle to the sandbox and print its verdict
8100
8186
  vet-skills publish <path> detonate and publish to your organization's private registry
8101
8187
  vet-skills why <skill> last decision and the rule behind it
8102
- vet-skills join <org> --token <t> [--email you@co] enroll this machine in an org
8188
+ vet-skills join <org> --token <t> [--email you@co] [--ci] enroll this machine (or a CI runner, seat-free) in an org
8103
8189
  vet-skills leave back to the personal policy
8104
8190
  vet-skills policy show|set <file> view or replace the local policy
8105
8191
  vet-skills block <skill> [--reason] add to the personal block list
8106
8192
  vet-skills status org, policy version, last report, cache size
8193
+ vet-skills sync pull the org policy and apply pending removals
8107
8194
  vet-skills hook install claude-code register the Claude Code PreToolUse hook
8108
8195
  vet-skills hook eval (called by the agent) read hook JSON on stdin
8109
8196
 
@@ -8184,7 +8271,7 @@ async function main() {
8184
8271
  return 1;
8185
8272
  }
8186
8273
  const dir = resolve(rest[0]);
8187
- if (!existsSync3(join3(dir, "SKILL.md")) && !existsSync3(join3(dir, "package.json"))) {
8274
+ if (!existsSync4(join4(dir, "SKILL.md")) && !existsSync4(join4(dir, "package.json"))) {
8188
8275
  console.log(`${c.blocked("\u2717")} ${dir} has no SKILL.md or package.json`);
8189
8276
  return 1;
8190
8277
  }
@@ -8227,7 +8314,7 @@ async function main() {
8227
8314
  return 1;
8228
8315
  }
8229
8316
  const dir = resolve(rest[0]);
8230
- if (!existsSync3(join3(dir, "SKILL.md"))) {
8317
+ if (!existsSync4(join4(dir, "SKILL.md"))) {
8231
8318
  console.log(`${c.blocked("\u2717")} ${dir} has no SKILL.md (name and version come from its frontmatter)`);
8232
8319
  return 1;
8233
8320
  }
@@ -8279,7 +8366,8 @@ async function main() {
8279
8366
  agent_kind: values.agent ?? "other",
8280
8367
  hook_version: VERSION,
8281
8368
  os: platform(),
8282
- installed: []
8369
+ installed: [],
8370
+ ci: values.ci === true
8283
8371
  });
8284
8372
  if (!res.ok) {
8285
8373
  console.error(`${c.blocked("\u2715")} Could not join ${slug}: ${res.error}`);
@@ -8364,11 +8452,24 @@ async function main() {
8364
8452
  console.log(`${c.verified("\u2713")} Policy updated to v${res.data.policy_version}`);
8365
8453
  }
8366
8454
  for (const a of res.data.pending_approvals) console.log(`approval ${a.skill} \xB7 ${a.status} \xB7 ${a.request_id}`);
8367
- for (const r of res.data.pending_removals) console.log(`${c.caution("!")} removal requested: ${r.skill} (${r.removal_id})`);
8455
+ if (res.data.pending_removals.length) await applyRemovals(res.data.pending_removals, (l) => console.log(l));
8368
8456
  } else console.log(c.caution(`report failed: ${res.error}`));
8369
8457
  }
8370
8458
  return 0;
8371
8459
  }
8460
+ case "sync": {
8461
+ if (!readOrg()) {
8462
+ console.log(c.muted("Not joined to an org; nothing to sync."));
8463
+ return 0;
8464
+ }
8465
+ const r = await sync((l) => console.log(l));
8466
+ if (!r.ok) {
8467
+ console.log(c.caution(`sync failed: ${r.error}`));
8468
+ return 1;
8469
+ }
8470
+ console.log(`${c.verified("\u2713")} in sync \xB7 ${r.removed} removal${r.removed === 1 ? "" : "s"} applied`);
8471
+ return 0;
8472
+ }
8372
8473
  case "hook": {
8373
8474
  const sub = rest[0];
8374
8475
  if (sub === "eval") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-skills",
3
- "version": "0.2.0",
3
+ "version": "0.4.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": {
@@ -10,21 +10,14 @@
10
10
  "dist",
11
11
  "README.md"
12
12
  ],
13
- "scripts": {
14
- "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --banner:js=\"import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);\" && node scripts/shebang.mjs",
15
- "dev": "tsx src/index.ts",
16
- "test": "node --import tsx --test test/*.test.ts",
17
- "typecheck": "tsc -p tsconfig.json --noEmit",
18
- "prepublishOnly": "pnpm build && node dist/index.js --version"
19
- },
20
13
  "dependencies": {},
21
14
  "devDependencies": {
22
- "@killerskills/policy": "workspace:*",
23
15
  "yaml": "^2.6.0",
24
16
  "@types/node": "^20",
25
17
  "tsx": "^4.19.0",
26
18
  "typescript": "^5",
27
- "esbuild": "^0.28.2"
19
+ "esbuild": "^0.28.2",
20
+ "@killerskills/policy": "0.1.0"
28
21
  },
29
22
  "engines": {
30
23
  "node": ">=20"
@@ -49,5 +42,11 @@
49
42
  ],
50
43
  "publishConfig": {
51
44
  "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --banner:js=\"import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);\" && node scripts/shebang.mjs",
48
+ "dev": "tsx src/index.ts",
49
+ "test": "node --import tsx --test test/*.test.ts",
50
+ "typecheck": "tsc -p tsconfig.json --noEmit"
52
51
  }
53
- }
52
+ }