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.
- package/dist/index.js +115 -14
- 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.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
|
|
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 =
|
|
8050
|
-
const path =
|
|
8051
|
-
if (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|