vet-skills 0.3.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 +89 -15
  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.3.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,9 +7991,59 @@ 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" },
@@ -8032,14 +8087,17 @@ async function hookEval(stdin) {
8032
8087
  }
8033
8088
  if (payload.tool_name !== "Bash" || !payload.tool_input?.command) return 0;
8034
8089
  const cmd2 = payload.tool_input.command;
8035
- 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;
8036
8091
  const found = extractInstall(cmd2);
8037
8092
  if (!found) return 0;
8038
8093
  const r = await check(found.q, { quiet: true, agentKind: "claude_code" });
8039
8094
  const out = renderOutcome(r);
8040
8095
  const remote = found.kind === "mcp" ? extractRemoteMcp(cmd2) : null;
8041
8096
  const watch = async () => {
8042
- if (remote && readOrg()) await report({ installed: [], removed: [], mcp_servers: [remote] }).catch(() => null);
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);
8043
8101
  };
8044
8102
  switch (r.decision.decision) {
8045
8103
  case "allow":
@@ -8073,11 +8131,11 @@ ${out}
8073
8131
  return 0;
8074
8132
  }
8075
8133
  function installClaudeCodeHook() {
8076
- const dir = join2(homedir2(), ".claude");
8077
- const path = join2(dir, "settings.json");
8078
- 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 });
8079
8137
  let settings = {};
8080
- if (existsSync2(path)) {
8138
+ if (existsSync3(path)) {
8081
8139
  try {
8082
8140
  settings = JSON.parse(readFileSync2(path, "utf8"));
8083
8141
  } catch {
@@ -8102,6 +8160,7 @@ var { values, positionals } = parseArgs({
8102
8160
  org: { type: "string" },
8103
8161
  yes: { type: "boolean", short: "y" },
8104
8162
  json: { type: "boolean" },
8163
+ ci: { type: "boolean" },
8105
8164
  email: { type: "string" },
8106
8165
  help: { type: "boolean", short: "h" },
8107
8166
  version: { type: "boolean", short: "v" },
@@ -8126,11 +8185,12 @@ var USAGE = `vet-skills ${VERSION} \u2014 check agent skills and MCP servers bef
8126
8185
  vet-skills detonate <path> [--publisher h] upload a local bundle to the sandbox and print its verdict
8127
8186
  vet-skills publish <path> detonate and publish to your organization's private registry
8128
8187
  vet-skills why <skill> last decision and the rule behind it
8129
- 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
8130
8189
  vet-skills leave back to the personal policy
8131
8190
  vet-skills policy show|set <file> view or replace the local policy
8132
8191
  vet-skills block <skill> [--reason] add to the personal block list
8133
8192
  vet-skills status org, policy version, last report, cache size
8193
+ vet-skills sync pull the org policy and apply pending removals
8134
8194
  vet-skills hook install claude-code register the Claude Code PreToolUse hook
8135
8195
  vet-skills hook eval (called by the agent) read hook JSON on stdin
8136
8196
 
@@ -8211,7 +8271,7 @@ async function main() {
8211
8271
  return 1;
8212
8272
  }
8213
8273
  const dir = resolve(rest[0]);
8214
- if (!existsSync3(join3(dir, "SKILL.md")) && !existsSync3(join3(dir, "package.json"))) {
8274
+ if (!existsSync4(join4(dir, "SKILL.md")) && !existsSync4(join4(dir, "package.json"))) {
8215
8275
  console.log(`${c.blocked("\u2717")} ${dir} has no SKILL.md or package.json`);
8216
8276
  return 1;
8217
8277
  }
@@ -8254,7 +8314,7 @@ async function main() {
8254
8314
  return 1;
8255
8315
  }
8256
8316
  const dir = resolve(rest[0]);
8257
- if (!existsSync3(join3(dir, "SKILL.md"))) {
8317
+ if (!existsSync4(join4(dir, "SKILL.md"))) {
8258
8318
  console.log(`${c.blocked("\u2717")} ${dir} has no SKILL.md (name and version come from its frontmatter)`);
8259
8319
  return 1;
8260
8320
  }
@@ -8306,7 +8366,8 @@ async function main() {
8306
8366
  agent_kind: values.agent ?? "other",
8307
8367
  hook_version: VERSION,
8308
8368
  os: platform(),
8309
- installed: []
8369
+ installed: [],
8370
+ ci: values.ci === true
8310
8371
  });
8311
8372
  if (!res.ok) {
8312
8373
  console.error(`${c.blocked("\u2715")} Could not join ${slug}: ${res.error}`);
@@ -8391,11 +8452,24 @@ async function main() {
8391
8452
  console.log(`${c.verified("\u2713")} Policy updated to v${res.data.policy_version}`);
8392
8453
  }
8393
8454
  for (const a of res.data.pending_approvals) console.log(`approval ${a.skill} \xB7 ${a.status} \xB7 ${a.request_id}`);
8394
- 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));
8395
8456
  } else console.log(c.caution(`report failed: ${res.error}`));
8396
8457
  }
8397
8458
  return 0;
8398
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
+ }
8399
8473
  case "hook": {
8400
8474
  const sub = rest[0];
8401
8475
  if (sub === "eval") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-skills",
3
- "version": "0.3.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
+ }