vet-skills 0.3.0 → 0.5.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/dist/index.js +124 -19
- 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
|
|
7618
|
-
import { resolve, join as
|
|
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.
|
|
7632
|
+
var VERSION = "0.5.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 });
|
|
@@ -7854,7 +7859,7 @@ async function check(q, opts = {}) {
|
|
|
7854
7859
|
let source = "api";
|
|
7855
7860
|
let offline = false;
|
|
7856
7861
|
let cacheAge;
|
|
7857
|
-
let res = await resolveSkill(q);
|
|
7862
|
+
let res = await resolveSkill(q, opts.registry);
|
|
7858
7863
|
if (!res.ok && !res.offline && org) {
|
|
7859
7864
|
const bare = q.replace(/^[^/]+\//, "");
|
|
7860
7865
|
const priv = await resolveSkill(`${org.slug}/${bare}`, "private");
|
|
@@ -7986,9 +7991,59 @@ function restrictionText(r) {
|
|
|
7986
7991
|
}
|
|
7987
7992
|
|
|
7988
7993
|
// src/hook.ts
|
|
7989
|
-
import { existsSync as
|
|
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" },
|
|
@@ -8016,10 +8071,41 @@ function extractRemoteMcp(command) {
|
|
|
8016
8071
|
const name = positional.find((a) => a !== url) ?? new URL(url).hostname;
|
|
8017
8072
|
return { name, url };
|
|
8018
8073
|
}
|
|
8074
|
+
function extractStdioPackage(command) {
|
|
8075
|
+
const tail = command.split(/\s--\s/)[1];
|
|
8076
|
+
if (!tail) return null;
|
|
8077
|
+
const args = (tail.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map((a) => a.replace(/^(['"])(.*)\1$/, "$2"));
|
|
8078
|
+
const [bin, ...rest2] = args;
|
|
8079
|
+
const strip = (s) => s.replace(/(.)@[^@/]+$/, "$1");
|
|
8080
|
+
if (/^(npx|bunx|pnpm)$/.test(bin ?? "")) {
|
|
8081
|
+
const pkg = rest2.find((a) => !a.startsWith("-") && a !== "dlx" && a !== "exec");
|
|
8082
|
+
return pkg ? { q: strip(pkg).replace(/^@/, ""), registry: "npm" } : null;
|
|
8083
|
+
}
|
|
8084
|
+
if (/^(uvx|pipx)$/.test(bin ?? "")) {
|
|
8085
|
+
const pkg = rest2.find((a) => !a.startsWith("-") && a !== "run");
|
|
8086
|
+
return pkg ? { q: strip(pkg).toLowerCase().replace(/\[.*\]$/, ""), registry: "pypi" } : null;
|
|
8087
|
+
}
|
|
8088
|
+
if (bin === "docker") {
|
|
8089
|
+
const img = rest2.find((a) => /^(ghcr\.io\/|docker\.io\/)?[\w.-]+\/[\w.-]+(:[\w.-]+)?$/.test(a) && !a.startsWith("-"));
|
|
8090
|
+
if (!img) return null;
|
|
8091
|
+
const m = img.replace(/:[\w.-]+$/, "").match(/mcp\/([\w.-]+)$/);
|
|
8092
|
+
return m ? { q: m[1], registry: "docker" } : null;
|
|
8093
|
+
}
|
|
8094
|
+
return null;
|
|
8095
|
+
}
|
|
8019
8096
|
function extractInstall(command) {
|
|
8020
8097
|
for (const p of INSTALL_PATTERNS) {
|
|
8021
8098
|
const m = command.match(p.re);
|
|
8022
|
-
if (m)
|
|
8099
|
+
if (!m) continue;
|
|
8100
|
+
const q = m[1].replace(/^['"]|['"]$/g, "");
|
|
8101
|
+
if (p.kind === "mcp") {
|
|
8102
|
+
const pkg = extractStdioPackage(command);
|
|
8103
|
+
if (pkg) return { q: pkg.q, kind: "mcp", registry: pkg.registry };
|
|
8104
|
+
const remote = extractRemoteMcp(command);
|
|
8105
|
+
if (remote) return { q: remote.name, kind: "mcp" };
|
|
8106
|
+
}
|
|
8107
|
+
if (p.kind === "plugin") return { q: q.split("@")[0], kind: "plugin", registry: "plugins" };
|
|
8108
|
+
return { q, kind: p.kind };
|
|
8023
8109
|
}
|
|
8024
8110
|
return null;
|
|
8025
8111
|
}
|
|
@@ -8032,14 +8118,17 @@ async function hookEval(stdin) {
|
|
|
8032
8118
|
}
|
|
8033
8119
|
if (payload.tool_name !== "Bash" || !payload.tool_input?.command) return 0;
|
|
8034
8120
|
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;
|
|
8121
|
+
if (/^\s*vet-skills\s+(check|why|status|sync|policy|detonate|hook|join|leave|block|publish)\b/.test(cmd2)) return 0;
|
|
8036
8122
|
const found = extractInstall(cmd2);
|
|
8037
8123
|
if (!found) return 0;
|
|
8038
|
-
const r = await check(found.q, { quiet: true, agentKind: "claude_code" });
|
|
8124
|
+
const r = await check(found.q, { quiet: true, agentKind: "claude_code", registry: found.registry });
|
|
8039
8125
|
const out = renderOutcome(r);
|
|
8040
8126
|
const remote = found.kind === "mcp" ? extractRemoteMcp(cmd2) : null;
|
|
8041
8127
|
const watch = async () => {
|
|
8042
|
-
if (
|
|
8128
|
+
if (!readOrg()) return;
|
|
8129
|
+
const res = await report({ installed: [], removed: [], ...remote ? { mcp_servers: [remote] } : {} }).catch(() => null);
|
|
8130
|
+
if (res?.ok && res.data.pending_removals.length) await applyRemovals(res.data.pending_removals, (l) => process.stderr.write(`vet-skills \xB7 ${l}
|
|
8131
|
+
`)).catch(() => 0);
|
|
8043
8132
|
};
|
|
8044
8133
|
switch (r.decision.decision) {
|
|
8045
8134
|
case "allow":
|
|
@@ -8073,11 +8162,11 @@ ${out}
|
|
|
8073
8162
|
return 0;
|
|
8074
8163
|
}
|
|
8075
8164
|
function installClaudeCodeHook() {
|
|
8076
|
-
const dir =
|
|
8077
|
-
const path =
|
|
8078
|
-
if (!
|
|
8165
|
+
const dir = join3(homedir3(), ".claude");
|
|
8166
|
+
const path = join3(dir, "settings.json");
|
|
8167
|
+
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
8079
8168
|
let settings = {};
|
|
8080
|
-
if (
|
|
8169
|
+
if (existsSync3(path)) {
|
|
8081
8170
|
try {
|
|
8082
8171
|
settings = JSON.parse(readFileSync2(path, "utf8"));
|
|
8083
8172
|
} catch {
|
|
@@ -8102,6 +8191,7 @@ var { values, positionals } = parseArgs({
|
|
|
8102
8191
|
org: { type: "string" },
|
|
8103
8192
|
yes: { type: "boolean", short: "y" },
|
|
8104
8193
|
json: { type: "boolean" },
|
|
8194
|
+
ci: { type: "boolean" },
|
|
8105
8195
|
email: { type: "string" },
|
|
8106
8196
|
help: { type: "boolean", short: "h" },
|
|
8107
8197
|
version: { type: "boolean", short: "v" },
|
|
@@ -8126,11 +8216,12 @@ var USAGE = `vet-skills ${VERSION} \u2014 check agent skills and MCP servers bef
|
|
|
8126
8216
|
vet-skills detonate <path> [--publisher h] upload a local bundle to the sandbox and print its verdict
|
|
8127
8217
|
vet-skills publish <path> detonate and publish to your organization's private registry
|
|
8128
8218
|
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
|
|
8219
|
+
vet-skills join <org> --token <t> [--email you@co] [--ci] enroll this machine (or a CI runner, seat-free) in an org
|
|
8130
8220
|
vet-skills leave back to the personal policy
|
|
8131
8221
|
vet-skills policy show|set <file> view or replace the local policy
|
|
8132
8222
|
vet-skills block <skill> [--reason] add to the personal block list
|
|
8133
8223
|
vet-skills status org, policy version, last report, cache size
|
|
8224
|
+
vet-skills sync pull the org policy and apply pending removals
|
|
8134
8225
|
vet-skills hook install claude-code register the Claude Code PreToolUse hook
|
|
8135
8226
|
vet-skills hook eval (called by the agent) read hook JSON on stdin
|
|
8136
8227
|
|
|
@@ -8154,7 +8245,7 @@ async function main() {
|
|
|
8154
8245
|
usage();
|
|
8155
8246
|
return 1;
|
|
8156
8247
|
}
|
|
8157
|
-
const r = await check(rest[0], { agentKind: values.agent });
|
|
8248
|
+
const r = await check(rest[0], { agentKind: values.agent, registry: values.registry });
|
|
8158
8249
|
if (values.json) {
|
|
8159
8250
|
console.log(JSON.stringify({ decision: r.decision, resolved: r.resolved, source: r.source, offline: r.offline }, null, 2));
|
|
8160
8251
|
return r.decision.decision === "block" ? 2 : 0;
|
|
@@ -8211,7 +8302,7 @@ async function main() {
|
|
|
8211
8302
|
return 1;
|
|
8212
8303
|
}
|
|
8213
8304
|
const dir = resolve(rest[0]);
|
|
8214
|
-
if (!
|
|
8305
|
+
if (!existsSync4(join4(dir, "SKILL.md")) && !existsSync4(join4(dir, "package.json"))) {
|
|
8215
8306
|
console.log(`${c.blocked("\u2717")} ${dir} has no SKILL.md or package.json`);
|
|
8216
8307
|
return 1;
|
|
8217
8308
|
}
|
|
@@ -8254,7 +8345,7 @@ async function main() {
|
|
|
8254
8345
|
return 1;
|
|
8255
8346
|
}
|
|
8256
8347
|
const dir = resolve(rest[0]);
|
|
8257
|
-
if (!
|
|
8348
|
+
if (!existsSync4(join4(dir, "SKILL.md"))) {
|
|
8258
8349
|
console.log(`${c.blocked("\u2717")} ${dir} has no SKILL.md (name and version come from its frontmatter)`);
|
|
8259
8350
|
return 1;
|
|
8260
8351
|
}
|
|
@@ -8306,7 +8397,8 @@ async function main() {
|
|
|
8306
8397
|
agent_kind: values.agent ?? "other",
|
|
8307
8398
|
hook_version: VERSION,
|
|
8308
8399
|
os: platform(),
|
|
8309
|
-
installed: []
|
|
8400
|
+
installed: [],
|
|
8401
|
+
ci: values.ci === true
|
|
8310
8402
|
});
|
|
8311
8403
|
if (!res.ok) {
|
|
8312
8404
|
console.error(`${c.blocked("\u2715")} Could not join ${slug}: ${res.error}`);
|
|
@@ -8391,11 +8483,24 @@ async function main() {
|
|
|
8391
8483
|
console.log(`${c.verified("\u2713")} Policy updated to v${res.data.policy_version}`);
|
|
8392
8484
|
}
|
|
8393
8485
|
for (const a of res.data.pending_approvals) console.log(`approval ${a.skill} \xB7 ${a.status} \xB7 ${a.request_id}`);
|
|
8394
|
-
|
|
8486
|
+
if (res.data.pending_removals.length) await applyRemovals(res.data.pending_removals, (l) => console.log(l));
|
|
8395
8487
|
} else console.log(c.caution(`report failed: ${res.error}`));
|
|
8396
8488
|
}
|
|
8397
8489
|
return 0;
|
|
8398
8490
|
}
|
|
8491
|
+
case "sync": {
|
|
8492
|
+
if (!readOrg()) {
|
|
8493
|
+
console.log(c.muted("Not joined to an org; nothing to sync."));
|
|
8494
|
+
return 0;
|
|
8495
|
+
}
|
|
8496
|
+
const r = await sync((l) => console.log(l));
|
|
8497
|
+
if (!r.ok) {
|
|
8498
|
+
console.log(c.caution(`sync failed: ${r.error}`));
|
|
8499
|
+
return 1;
|
|
8500
|
+
}
|
|
8501
|
+
console.log(`${c.verified("\u2713")} in sync \xB7 ${r.removed} removal${r.removed === 1 ? "" : "s"} applied`);
|
|
8502
|
+
return 0;
|
|
8503
|
+
}
|
|
8399
8504
|
case "hook": {
|
|
8400
8505
|
const sub = rest[0];
|
|
8401
8506
|
if (sub === "eval") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vet-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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
|
+
}
|