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.
- package/dist/index.js +89 -15
- 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,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" },
|
|
@@ -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 (
|
|
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 =
|
|
8077
|
-
const path =
|
|
8078
|
-
if (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
+
"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
|
+
}
|