skilldrop-cli 0.16.25 → 0.16.27
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/bin/governance.js +212 -0
- package/bin/skilldrop.js +188 -4
- package/contracts/policy.example.json +14 -0
- package/contracts/policy.schema.json +46 -0
- package/package.json +1 -1
- package/packs/solution-architect/skills/interactive-diagram/SKILL.md +7 -7
- package/packs/solution-architect/skills/interactive-diagram/manifest.json +1 -1
- package/packs/solution-architect/skills/interactive-diagram/scripts/build_diagram.py +1 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/* Organisation governance (RFC-0050, OWASP AST09): a policy the installer enforces, an audit
|
|
2
|
+
log that shows when its history was edited, and a check that a mirror still matches what was
|
|
3
|
+
packaged. Node's standard library only, like the rest of the CLI.
|
|
4
|
+
|
|
5
|
+
The policy is a JSON file an organisation hands out (MDM, a dotfiles repo, an image):
|
|
6
|
+
--policy <file> > SKILLDROP_POLICY > ~/.config/skilldrop/policy.json
|
|
7
|
+
A policy that is named but missing, unreadable, or carries a key this version doesn't know
|
|
8
|
+
stops the command: a policy that fails open is not a policy. */
|
|
9
|
+
"use strict";
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const os = require("os");
|
|
13
|
+
const crypto = require("crypto");
|
|
14
|
+
const { execFileSync } = require("child_process");
|
|
15
|
+
|
|
16
|
+
const SEVERITY = { "🟥": 3, "🟧": 2, "🟨": 1 }; // scan findings; ⚪ (fixtures) never count
|
|
17
|
+
const MAX = { none: 0, low: 1, medium: 2, high: 3 }; // policy scan.max: the worst finding allowed
|
|
18
|
+
const KEYS = {
|
|
19
|
+
"": ["version", "catalogues", "skills", "scan", "audit", "note"],
|
|
20
|
+
catalogues: ["allow", "require_pinned"],
|
|
21
|
+
skills: ["allow", "block"],
|
|
22
|
+
scan: ["max"],
|
|
23
|
+
audit: ["log", "actor", "salt_env"],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function die(msg) { console.error(`error: ${msg}`); process.exit(1); }
|
|
27
|
+
const sha256 = (s) => crypto.createHash("sha256").update(s).digest("hex");
|
|
28
|
+
const tilde = (p) => (p.startsWith(os.homedir()) ? "~" + p.slice(os.homedir().length) : p);
|
|
29
|
+
const untilde = (p) => (p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p);
|
|
30
|
+
|
|
31
|
+
function policyPath(flags) {
|
|
32
|
+
if (flags.policy && flags.policy !== true) return { file: path.resolve(String(flags.policy)), named: true };
|
|
33
|
+
if (process.env.SKILLDROP_POLICY) return { file: path.resolve(untilde(process.env.SKILLDROP_POLICY)), named: true };
|
|
34
|
+
const dflt = path.join(os.homedir(), ".config", "skilldrop", "policy.json");
|
|
35
|
+
return { file: dflt, named: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* The policy in force, or null when none is set. */
|
|
39
|
+
function load(flags) {
|
|
40
|
+
const { file, named } = policyPath(flags || {});
|
|
41
|
+
if (!fs.existsSync(file)) {
|
|
42
|
+
if (named) die(`policy ${file} does not exist — nothing was changed`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
let raw, p;
|
|
46
|
+
try { raw = fs.readFileSync(file, "utf8"); p = JSON.parse(raw); }
|
|
47
|
+
catch (e) { die(`policy ${file} is not valid JSON (${e.message}) — nothing was changed`); }
|
|
48
|
+
const problems = [];
|
|
49
|
+
const closed = (obj, where) => {
|
|
50
|
+
if (obj === undefined) return;
|
|
51
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return problems.push(`${where || "the policy"} must be an object`);
|
|
52
|
+
for (const k of Object.keys(obj)) if (!KEYS[where].includes(k)) problems.push(`unknown key '${where ? where + "." : ""}${k}'`);
|
|
53
|
+
};
|
|
54
|
+
closed(p, "");
|
|
55
|
+
for (const k of ["catalogues", "skills", "scan", "audit"]) closed(p[k], k);
|
|
56
|
+
if (p.version !== 1) problems.push("version must be 1");
|
|
57
|
+
const list = (v, where) => { if (v !== undefined && !(Array.isArray(v) && v.every((x) => typeof x === "string"))) problems.push(`${where} must be a list of strings`); };
|
|
58
|
+
list(p.catalogues && p.catalogues.allow, "catalogues.allow");
|
|
59
|
+
list(p.skills && p.skills.allow, "skills.allow");
|
|
60
|
+
list(p.skills && p.skills.block, "skills.block");
|
|
61
|
+
if (p.catalogues && p.catalogues.require_pinned !== undefined && typeof p.catalogues.require_pinned !== "boolean")
|
|
62
|
+
problems.push("catalogues.require_pinned must be true or false");
|
|
63
|
+
if (p.scan && p.scan.max !== undefined && !(p.scan.max in MAX)) problems.push(`scan.max must be one of ${Object.keys(MAX).join(", ")}`);
|
|
64
|
+
if (p.audit) {
|
|
65
|
+
const a = p.audit.actor;
|
|
66
|
+
const ok = a === undefined || ["none", "pseudonym", "os-user"].includes(a) ||
|
|
67
|
+
(a && typeof a === "object" && Object.keys(a).length === 1 && typeof a.fixed === "string" && a.fixed.trim());
|
|
68
|
+
if (!ok) problems.push('audit.actor must be "none", "pseudonym", "os-user" or {"fixed": "<name>"}');
|
|
69
|
+
if (p.audit.log !== undefined && typeof p.audit.log !== "string") problems.push("audit.log must be a path");
|
|
70
|
+
}
|
|
71
|
+
if (problems.length) die(`policy ${file} is not usable — nothing was changed:\n ${problems.join("\n ")}`);
|
|
72
|
+
return Object.assign(p, { file, hash: sha256(raw).slice(0, 16) });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const glob = (pat) => new RegExp("^" + pat.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$", "i");
|
|
76
|
+
const matches = (name, pats) => (pats || []).some((p) => glob(p).test(name));
|
|
77
|
+
|
|
78
|
+
/* A catalogue source as the policy compares it: "bundled", an absolute path, or a git URL
|
|
79
|
+
without its #ref, trailing slash or .git. */
|
|
80
|
+
function sourceKey(source) {
|
|
81
|
+
if (!source || source === "bundled") return "bundled";
|
|
82
|
+
const i = source.lastIndexOf("#");
|
|
83
|
+
const url = i > 0 ? source.slice(0, i) : source;
|
|
84
|
+
if (fs.existsSync(url)) return path.resolve(url);
|
|
85
|
+
return url.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Why the policy refuses these units, as [{name, why}]; an empty list means allowed.
|
|
89
|
+
worst: {name: "🟥" | "🟧" | "🟨" | undefined}, the worst scan finding per unit. */
|
|
90
|
+
function check(policy, { source, pinned, names, worst = {}, kind = "skill" }) {
|
|
91
|
+
if (!policy) return [];
|
|
92
|
+
const out = [];
|
|
93
|
+
const key = sourceKey(source);
|
|
94
|
+
const cats = policy.catalogues || {};
|
|
95
|
+
if (cats.allow && !cats.allow.some((a) => (a === "bundled" ? key === "bundled" : matches(key, [sourceKey(a)]))))
|
|
96
|
+
for (const n of names) out.push({ name: n, why: `catalogue '${key}' is not in the policy's allowed list` });
|
|
97
|
+
else if (cats.require_pinned && key !== "bundled" && !pinned)
|
|
98
|
+
for (const n of names) out.push({ name: n, why: `catalogue '${key}' must be pinned to a commit (--from <url>#<commit>)` });
|
|
99
|
+
const sk = policy.skills || {};
|
|
100
|
+
for (const n of names) {
|
|
101
|
+
if (matches(n, sk.block)) out.push({ name: n, why: `${kind} '${n}' is blocked by the policy` });
|
|
102
|
+
else if (sk.allow && !matches(n, sk.allow)) out.push({ name: n, why: `${kind} '${n}' is not in the policy's allowed list` });
|
|
103
|
+
const max = policy.scan && policy.scan.max;
|
|
104
|
+
if (max !== undefined && worst[n] && SEVERITY[worst[n]] > MAX[max])
|
|
105
|
+
out.push({ name: n, why: `the supply-chain scan found a ${worst[n]} pattern, above the policy's limit (${max})` });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ---------- audit log ----------
|
|
111
|
+
One JSON object per line. Each line carries the SHA-256 of the line before it, so a line
|
|
112
|
+
edited or removed afterwards breaks the chain, and `skilldrop audit verify` says where. */
|
|
113
|
+
function auditFile(policy) {
|
|
114
|
+
const env = process.env.SKILLDROP_AUDIT_LOG;
|
|
115
|
+
if (!(policy && policy.audit) && !env) return null;
|
|
116
|
+
const f = path.resolve(untilde(env || (policy.audit.log || "~/.config/skilldrop/audit.jsonl")));
|
|
117
|
+
// Never inside a git working tree, where it could be committed.
|
|
118
|
+
try {
|
|
119
|
+
const dir = path.dirname(f);
|
|
120
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
121
|
+
const top = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
122
|
+
if (top) die(`the audit log ${tilde(f)} is inside a git working tree (${tilde(top)}); keep it outside any repository`);
|
|
123
|
+
} catch (e) { /* not inside a repository (or no git): good */ }
|
|
124
|
+
return f;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function actor(policy) {
|
|
128
|
+
const mode = (policy && policy.audit && policy.audit.actor) || "pseudonym";
|
|
129
|
+
if (mode === "none") return null;
|
|
130
|
+
if (typeof mode === "object") return mode.fixed;
|
|
131
|
+
const user = (() => { try { return os.userInfo().username; } catch (e) { return "unknown"; } })();
|
|
132
|
+
if (mode === "os-user") return user;
|
|
133
|
+
const saltVar = (policy && policy.audit && policy.audit.salt_env) || "SKILLDROP_AUDIT_SALT";
|
|
134
|
+
const salt = process.env[saltVar];
|
|
135
|
+
if (!salt) die(`the audit log names people by pseudonym, which needs a secret salt: set ${saltVar} (the organisation keeps it), or set audit.actor to "none"`);
|
|
136
|
+
return "p:" + crypto.createHmac("sha256", salt).update(`${user}@${os.hostname()}`).digest("hex").slice(0, 16);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function lastLine(f) {
|
|
140
|
+
if (!fs.existsSync(f)) return null;
|
|
141
|
+
const lines = fs.readFileSync(f, "utf8").split("\n").filter(Boolean);
|
|
142
|
+
return lines.length ? lines[lines.length - 1] : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Everything the audit log needs (where it goes, who is acting), worked out before a command
|
|
146
|
+
changes anything: an install must not land and then fail to be recorded. */
|
|
147
|
+
const ready = new Map();
|
|
148
|
+
function prepare(policy) {
|
|
149
|
+
const key = policy || "env";
|
|
150
|
+
if (ready.has(key)) return ready.get(key);
|
|
151
|
+
const f = auditFile(policy);
|
|
152
|
+
const r = f ? { f, who: actor(policy) } : null;
|
|
153
|
+
ready.set(key, r);
|
|
154
|
+
return r;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Append one event per unit. Does nothing when no audit log is configured. */
|
|
158
|
+
function audit(policy, action, units, extra) {
|
|
159
|
+
const r = prepare(policy);
|
|
160
|
+
if (!r || !units.length) return;
|
|
161
|
+
const { f, who } = r;
|
|
162
|
+
for (const u of units) {
|
|
163
|
+
const prev = lastLine(f);
|
|
164
|
+
const entry = Object.assign({ ts: new Date().toISOString(), action }, u, extra || {},
|
|
165
|
+
{ actor: who, policy: policy ? policy.hash : null, prev: prev ? sha256(prev) : "0".repeat(64) });
|
|
166
|
+
if (entry.target) entry.target = tilde(entry.target);
|
|
167
|
+
if (entry.source) entry.source = tilde(entry.source); // a catalogue on local disk names no home directory
|
|
168
|
+
fs.appendFileSync(f, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function verifyAudit(file) {
|
|
173
|
+
const f = path.resolve(untilde(file));
|
|
174
|
+
if (!fs.existsSync(f)) die(`no audit log at ${tilde(f)}`);
|
|
175
|
+
const lines = fs.readFileSync(f, "utf8").split("\n").filter(Boolean);
|
|
176
|
+
let prev = "0".repeat(64);
|
|
177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
178
|
+
let e;
|
|
179
|
+
try { e = JSON.parse(lines[i]); } catch (err) { return { ok: false, line: i + 1, why: "not valid JSON", count: lines.length }; }
|
|
180
|
+
if (e.prev !== prev) return { ok: false, line: i + 1, why: "does not follow the line before it: a line was edited, removed or inserted", count: lines.length };
|
|
181
|
+
prev = sha256(lines[i]);
|
|
182
|
+
}
|
|
183
|
+
return { ok: true, count: lines.length };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ---------- mirrors ----------
|
|
187
|
+
MIRROR.json (skilldrop package) records a SHA-256 for every file it copied. */
|
|
188
|
+
function verifyMirror(dir) {
|
|
189
|
+
const root = path.resolve(dir);
|
|
190
|
+
const mf = path.join(root, "MIRROR.json");
|
|
191
|
+
if (!fs.existsSync(mf)) die(`${root} has no MIRROR.json — it wasn't made by skilldrop package`);
|
|
192
|
+
let m;
|
|
193
|
+
try { m = JSON.parse(fs.readFileSync(mf, "utf8")); } catch (e) { die(`${mf} is not valid JSON`); }
|
|
194
|
+
const want = m.files || {};
|
|
195
|
+
const found = {};
|
|
196
|
+
const walk = (d) => {
|
|
197
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
198
|
+
if (e.name === ".git") continue;
|
|
199
|
+
const p = path.join(d, e.name);
|
|
200
|
+
if (e.isDirectory()) walk(p);
|
|
201
|
+
else found[path.relative(root, p).split(path.sep).join("/")] = p;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
walk(root);
|
|
205
|
+
const own = new Set(["MIRROR.json", "catalogue.json"]);
|
|
206
|
+
const missing = Object.keys(want).filter((f) => !found[f]);
|
|
207
|
+
const changed = Object.keys(want).filter((f) => found[f] && sha256(fs.readFileSync(found[f])) !== want[f]);
|
|
208
|
+
const extra = Object.keys(found).filter((f) => !(f in want) && !own.has(f) && !/^packs\/[^/]+\/pack\.json$/.test(f));
|
|
209
|
+
return { source: m.source, created: m.created, files: Object.keys(want).length, missing, changed, extra };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { load, check, prepare, audit, verifyAudit, verifyMirror, sourceKey, tilde, SEVERITY };
|
package/bin/skilldrop.js
CHANGED
|
@@ -15,6 +15,7 @@ const path = require("path");
|
|
|
15
15
|
const os = require("os");
|
|
16
16
|
const { execFileSync } = require("child_process");
|
|
17
17
|
const crypto = require("crypto");
|
|
18
|
+
const gov = require("./governance"); // org policy, audit log, mirror check (RFC-0050)
|
|
18
19
|
|
|
19
20
|
const ROOT = path.resolve(__dirname, "..");
|
|
20
21
|
const LEDGER = ".skilldrop.json";
|
|
@@ -75,6 +76,11 @@ Usage:
|
|
|
75
76
|
skilldrop init-catalogue <dir> [--pack <name>] start a private catalogue for your team's skills
|
|
76
77
|
skilldrop package <dir> [--pack a,b] [--skills x,y] [--agents] copy a vetted subset of a catalogue
|
|
77
78
|
into a standalone one to host internally (writes MIRROR.json)
|
|
79
|
+
skilldrop inventory [--json] [--label <machine>] every skilldrop install on this machine, with
|
|
80
|
+
what the policy thinks of each (RFC-0050)
|
|
81
|
+
skilldrop inventory --merge <files...> combine inventory reports from many machines into one view
|
|
82
|
+
skilldrop audit verify|show [<file>] check the audit log's chain, or show its last entries
|
|
83
|
+
skilldrop verify-mirror <dir> check a mirror against the SHA-256s its MIRROR.json recorded
|
|
78
84
|
skilldrop --version print the CLI version
|
|
79
85
|
skilldrop bootstrap add the skilldrop marketplace to ~/.claude/settings.json
|
|
80
86
|
(idempotent — safe to run in onboarding scripts)
|
|
@@ -98,6 +104,12 @@ Install/update/uninstall targets (pick one):
|
|
|
98
104
|
--local project scope, hidden from git via .git/info/exclude — try skills in a
|
|
99
105
|
repo you don't own without touching its tracked files or .gitignore
|
|
100
106
|
|
|
107
|
+
Organisation policy (RFC-0050):
|
|
108
|
+
--policy <file> the policy to enforce; also SKILLDROP_POLICY, else ~/.config/skilldrop/policy.json.
|
|
109
|
+
It can allow only some catalogues (and require them pinned), allow or block skills,
|
|
110
|
+
cap the supply-chain scan's findings, and turn on an audit log. A policy that is
|
|
111
|
+
named but missing or unreadable stops the command.
|
|
112
|
+
|
|
101
113
|
Options:
|
|
102
114
|
--dry-run install | update | uninstall | new-skill: print what would change, change nothing
|
|
103
115
|
--yes skip the confirmation uninstall and update --force ask for at a terminal
|
|
@@ -131,7 +143,8 @@ function parseArgs(argv) {
|
|
|
131
143
|
const out = { _: [], flags: {} };
|
|
132
144
|
for (let i = 0; i < argv.length; i++) {
|
|
133
145
|
const a = argv[i];
|
|
134
|
-
if (a === "--pack" || a === "--ide" || a === "--dest" || a === "--from" || a === "--panel" || a === "--profile" || a === "--tier" || a === "--skills" || a === "--days"
|
|
146
|
+
if (a === "--pack" || a === "--ide" || a === "--dest" || a === "--from" || a === "--panel" || a === "--profile" || a === "--tier" || a === "--skills" || a === "--days" ||
|
|
147
|
+
a === "--policy" || a === "--merge" || a === "--label" || a === "--last") out.flags[a.slice(2)] = argv[++i];
|
|
135
148
|
else if (a.startsWith("--")) out.flags[a.slice(2)] = true;
|
|
136
149
|
else out._.push(a);
|
|
137
150
|
}
|
|
@@ -420,6 +433,35 @@ function gate(cat, names) {
|
|
|
420
433
|
if (bad) die(`${bad} skill(s) failed the structural check — nothing was installed`);
|
|
421
434
|
}
|
|
422
435
|
|
|
436
|
+
/* RFC-0050: the worst scan finding per skill, for a policy's scan.max. Eval fixtures and
|
|
437
|
+
example inputs (⚪) never count. */
|
|
438
|
+
function worstFindings(cat, names) {
|
|
439
|
+
const out = {};
|
|
440
|
+
for (const s of names) {
|
|
441
|
+
let w;
|
|
442
|
+
for (const f of scanSkill(cat, s)) if (!f.fixture && (!w || gov.SEVERITY[f.sev] > gov.SEVERITY[w])) w = f.sev;
|
|
443
|
+
if (w) out[s] = w;
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Refuse the whole install when the policy in force forbids any part of it. */
|
|
449
|
+
function enforcePolicy(flags, cat, names, kind) {
|
|
450
|
+
const policy = gov.load(flags);
|
|
451
|
+
gov.prepare(policy); // an audit log that can't be written stops the install before it starts
|
|
452
|
+
if (!policy || !names.length) return policy;
|
|
453
|
+
const worst = kind === "skill" && policy.scan && policy.scan.max !== undefined ? worstFindings(cat, names) : {};
|
|
454
|
+
const problems = gov.check(policy, { source: cat.source, pinned: cat.pinned, names, worst, kind });
|
|
455
|
+
if (problems.length) {
|
|
456
|
+
for (const pr of problems) console.error(`refused ${pr.name}: ${pr.why}`);
|
|
457
|
+
die(`the policy in ${gov.tilde(policy.file)} refuses ${new Set(problems.map((x) => x.name)).size} ${kind}(s) — nothing was installed`);
|
|
458
|
+
}
|
|
459
|
+
return policy;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const auditUnit = (cat, name, kind, version, dest) =>
|
|
463
|
+
({ kind, name, version: version || null, source: cat.source, commit: cat.commit || null, pinned: !!cat.pinned, target: dest });
|
|
464
|
+
|
|
423
465
|
/* ---------- supply-chain scan (RFC-0022) ----------
|
|
424
466
|
A skill is instruction prose an agent will obey, and its scripts are code that runs on the
|
|
425
467
|
user's machine — so installing a third-party catalog is a supply-chain event. Anthropic's own
|
|
@@ -1221,6 +1263,7 @@ function install(args) {
|
|
|
1221
1263
|
}
|
|
1222
1264
|
}
|
|
1223
1265
|
gate(cat, names);
|
|
1266
|
+
const policy = enforcePolicy(args.flags, cat, names, "skill");
|
|
1224
1267
|
const { dest, ide } = target(args.flags);
|
|
1225
1268
|
if (args.flags["dry-run"]) return planInstall(cat, names, dest, ide, args.flags);
|
|
1226
1269
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -1233,6 +1276,7 @@ function install(args) {
|
|
|
1233
1276
|
console.log(`installed ${s}@${m.version} -> ${dest}`);
|
|
1234
1277
|
}
|
|
1235
1278
|
saveLedger(l);
|
|
1279
|
+
gov.audit(policy, "install", names.map((s) => auditUnit(cat, s, "skill", lver(l.data[s]), dest)));
|
|
1236
1280
|
console.log(`\n${names.length} skill(s) installed (${ide}).`);
|
|
1237
1281
|
if (notes.length) console.log(`\nCleanup:\n${notes.join("\n")}`);
|
|
1238
1282
|
const local = !!args.flags.local;
|
|
@@ -1354,7 +1398,10 @@ function update(args) {
|
|
|
1354
1398
|
}
|
|
1355
1399
|
}
|
|
1356
1400
|
if (dry) return planUpdate(dest, ide, l, rows, args.flags);
|
|
1357
|
-
|
|
1401
|
+
const policy = gov.load(args.flags);
|
|
1402
|
+
gov.prepare(policy);
|
|
1403
|
+
const done = [];
|
|
1404
|
+
let n = 0, conflicts = 0, refusedByPolicy = 0;
|
|
1358
1405
|
const silent = rows.filter((r) => r.current === r.installed && r.changed.length && !args.flags.changed);
|
|
1359
1406
|
for (const r of rows) {
|
|
1360
1407
|
if (r.retired) { console.log(`skip ${r.retired}\n Remove it with: skilldrop uninstall ${r.s}${targetHint(args.flags)}`); continue; }
|
|
@@ -1362,6 +1409,9 @@ function update(args) {
|
|
|
1362
1409
|
if (!isDue(r)) continue;
|
|
1363
1410
|
const problems = unitCheck(r.cat, r.s, r.loop);
|
|
1364
1411
|
if (problems.length) { console.log(`skip ${unitLabel(r)}: fails structural check in '${r.src}' (${problems[0]})`); continue; }
|
|
1412
|
+
const refused = gov.check(policy, { source: r.src, pinned: r.pinned, names: [r.s], kind: r.loop ? "loop" : "skill",
|
|
1413
|
+
worst: !r.loop && policy && policy.scan && policy.scan.max !== undefined ? worstFindings(r.cat, [r.s]) : {} });
|
|
1414
|
+
if (refused.length) { console.log(`skip ${unitLabel(r)}: the policy refuses it (${refused[0].why})`); refusedByPolicy++; continue; }
|
|
1365
1415
|
// No baseline (installed before RFC-0032, or a loop installed before 0.16.11) or --force:
|
|
1366
1416
|
// overwrite as before, and record one.
|
|
1367
1417
|
const entry = l.data[r.s];
|
|
@@ -1377,9 +1427,12 @@ function update(args) {
|
|
|
1377
1427
|
conflicts += kept.length;
|
|
1378
1428
|
}
|
|
1379
1429
|
n++;
|
|
1430
|
+
done.push(auditUnit(r.cat, r.s, r.loop ? "loop" : "skill", r.current, dest));
|
|
1380
1431
|
}
|
|
1381
1432
|
saveLedger(l);
|
|
1382
|
-
|
|
1433
|
+
gov.audit(policy, "update", done);
|
|
1434
|
+
if (refusedByPolicy) console.log(`\n${refusedByPolicy} not updated because the policy refuses them; they stay as they are. Remove them with skilldrop uninstall, or change the policy.`);
|
|
1435
|
+
else console.log(n ? `\n${n} skill(s) or loop(s) updated.` : (silent.length ? "no version changes." : rows.some((r) => r.retired) ? "nothing else to update." : "everything up to date."));
|
|
1383
1436
|
if (silent.length)
|
|
1384
1437
|
console.log(`\nNot taken: ${silent.map((r) => r.s).join(", ")} changed upstream without a version bump.` +
|
|
1385
1438
|
`\n Read the change (skilldrop diff <skill>), then take it with: skilldrop update --changed`);
|
|
@@ -1484,11 +1537,14 @@ function uninstall(args) {
|
|
|
1484
1537
|
const what = `Remove ${present.length} skill(s) from ${dest}: ${present.join(", ")}` +
|
|
1485
1538
|
(edits.length ? `\n (${edits.length} of the files have your edits: ${edits.join(", ")})` : "") + "?";
|
|
1486
1539
|
if (!confirm(what, args.flags)) return console.log("cancelled — nothing removed.");
|
|
1540
|
+
gov.prepare(gov.load(args.flags));
|
|
1487
1541
|
for (const s of present) {
|
|
1488
1542
|
fs.rmSync(path.join(dest, s), { recursive: true, force: true });
|
|
1489
1543
|
const w = wiringPath(ide, dest, s);
|
|
1490
1544
|
if (w) fs.rmSync(w, { force: true });
|
|
1491
1545
|
removeHooksFor(s, dest, ide);
|
|
1546
|
+
gov.audit(gov.load(args.flags), "uninstall", [{ kind: "skill", name: s, version: l.data[s] ? lver(l.data[s]) : null,
|
|
1547
|
+
source: l.data[s] ? lsrc(l.data[s]) : null, target: dest }]);
|
|
1492
1548
|
delete l.data[s];
|
|
1493
1549
|
console.log(`removed ${s} from ${dest}`);
|
|
1494
1550
|
}
|
|
@@ -1919,6 +1975,12 @@ function installLoops(args) {
|
|
|
1919
1975
|
if (problems.length) bad++;
|
|
1920
1976
|
}
|
|
1921
1977
|
if (bad) die(`${bad} loop(s) failed the structural check — nothing was installed`);
|
|
1978
|
+
const policy = enforcePolicy(args.flags, cat, names, "loop");
|
|
1979
|
+
if (!args.flags["no-skills"]) {
|
|
1980
|
+
const stage = [];
|
|
1981
|
+
for (const n of names) for (const sk of loopSkills(cat, n)) if (!stage.includes(sk)) stage.push(sk);
|
|
1982
|
+
enforcePolicy(args.flags, cat, stage, "skill");
|
|
1983
|
+
}
|
|
1922
1984
|
|
|
1923
1985
|
const { dest, ide } = target(args.flags);
|
|
1924
1986
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -1929,6 +1991,7 @@ function installLoops(args) {
|
|
|
1929
1991
|
console.log(`installed loop ${n} -> ${path.join(dest, n)}`);
|
|
1930
1992
|
}
|
|
1931
1993
|
saveLedger(l);
|
|
1994
|
+
gov.audit(policy, "install", names.map((n) => auditUnit(cat, n, "loop", lver(l.data[n]), dest)));
|
|
1932
1995
|
if (args.flags.local) addLocal(dest, ide, names);
|
|
1933
1996
|
|
|
1934
1997
|
const wanted = [];
|
|
@@ -1959,6 +2022,8 @@ function uninstallLoops(args) {
|
|
|
1959
2022
|
fs.rmSync(path.join(dest, n), { recursive: true, force: true });
|
|
1960
2023
|
const w = wiringPath(ide, dest, n);
|
|
1961
2024
|
if (w) fs.rmSync(w, { force: true });
|
|
2025
|
+
gov.audit(gov.load(args.flags), "uninstall", [{ kind: "loop", name: n, version: l.data[n] ? lver(l.data[n]) : null,
|
|
2026
|
+
source: l.data[n] ? lsrc(l.data[n]) : null, target: dest }]);
|
|
1962
2027
|
delete l.data[n];
|
|
1963
2028
|
console.log(`removed loop ${n} from ${dest} (stage skills left in place)`);
|
|
1964
2029
|
}
|
|
@@ -1981,6 +2046,7 @@ function installAgents(args) {
|
|
|
1981
2046
|
if (problems.length) bad++;
|
|
1982
2047
|
}
|
|
1983
2048
|
if (bad) die(`${bad} agent(s) failed the structural check — nothing was installed`);
|
|
2049
|
+
const policy = enforcePolicy(args.flags, cat, names, "agent");
|
|
1984
2050
|
|
|
1985
2051
|
const { dest, ide } = agentTarget(args.flags);
|
|
1986
2052
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -1993,6 +2059,7 @@ function installAgents(args) {
|
|
|
1993
2059
|
console.log(`installed ${a} -> ${path.join(dest, file)}`);
|
|
1994
2060
|
}
|
|
1995
2061
|
saveLedger(l);
|
|
2062
|
+
gov.audit(policy, "install", names.map((a) => auditUnit(cat, a, "agent", null, dest)));
|
|
1996
2063
|
console.log(`\n${names.length} agent(s) installed (${ide}).`);
|
|
1997
2064
|
if (warns.length) console.log(`\nTool mapping:\n${warns.join("\n")}`);
|
|
1998
2065
|
if (ide === "claude") console.log("Delegate by name, e.g. \"use the devils-advocate agent on this diff\".");
|
|
@@ -2125,6 +2192,10 @@ function validateCmd(args) {
|
|
|
2125
2192
|
const cat = resolveCatalog(args.flags.from);
|
|
2126
2193
|
const names = skillsIn(cat);
|
|
2127
2194
|
let bad = 0;
|
|
2195
|
+
// RFC-0050: this CLI accepts the policy example the contract describes (validate.py checks the
|
|
2196
|
+
// other half), so the schema and the enforcement can't drift apart unnoticed.
|
|
2197
|
+
const example = path.join(cat.dir || ROOT, "contracts", "policy.example.json");
|
|
2198
|
+
if (!args.flags.from && fs.existsSync(example)) gov.load({ policy: example });
|
|
2128
2199
|
for (const s of names) {
|
|
2129
2200
|
const problems = checkSkill(cat, s);
|
|
2130
2201
|
for (const f of scanSkill(cat, s).filter((x) => x.id.startsWith("undeclared-")))
|
|
@@ -2149,6 +2220,118 @@ const BOOTSTRAP_MARKETPLACE_KEY = "skilldrop";
|
|
|
2149
2220
|
const BOOTSTRAP_GITHUB_OWNER = "sananthanarayan";
|
|
2150
2221
|
const BOOTSTRAP_GITHUB_REPO = "skilldrop";
|
|
2151
2222
|
|
|
2223
|
+
/* ---------- inventory (RFC-0050) ----------
|
|
2224
|
+
Every skilldrop install on this machine, from each target's ledger: one report an
|
|
2225
|
+
organisation can collect, and --merge to combine many into one view. Paths under the home
|
|
2226
|
+
directory are written as ~ so a report names no one. */
|
|
2227
|
+
function inventoryTargets(flags) {
|
|
2228
|
+
if (flags.dest) return [{ dest: path.resolve(flags.dest), ide: "generic", scope: "dest" }];
|
|
2229
|
+
const out = [];
|
|
2230
|
+
for (const [ide, [proj, user]] of Object.entries(SKILL_DIRS)) {
|
|
2231
|
+
out.push({ dest: path.resolve(...proj), ide, scope: "project" });
|
|
2232
|
+
if (user) out.push({ dest: path.join(os.homedir(), ...user), ide, scope: "user" });
|
|
2233
|
+
}
|
|
2234
|
+
for (const ide of ["claude", "codex"]) for (const project of [true, false]) {
|
|
2235
|
+
try { const t = agentTarget({ ide, project }); out.push({ dest: t.dest, ide, scope: project ? "project" : "user" }); } catch (e) { /* no target */ }
|
|
2236
|
+
}
|
|
2237
|
+
const seen = new Set();
|
|
2238
|
+
return out.filter((t) => !seen.has(t.dest) && seen.add(t.dest));
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
function inventory(args) {
|
|
2242
|
+
if (args.flags.merge) return mergeInventories([String(args.flags.merge), ...args._], args.flags);
|
|
2243
|
+
const policy = gov.load(args.flags);
|
|
2244
|
+
const installs = [];
|
|
2245
|
+
for (const t of inventoryTargets(args.flags)) {
|
|
2246
|
+
const l = ledger(t.dest);
|
|
2247
|
+
for (const name of Object.keys(l.data).sort()) {
|
|
2248
|
+
const e = l.data[name];
|
|
2249
|
+
const kind = isLoopEntry(e) ? "loop" : e && e.file ? "agent" : "skill";
|
|
2250
|
+
const src = lsrc(e);
|
|
2251
|
+
const pinned = isPinned(src);
|
|
2252
|
+
const refused = gov.check(policy, { source: src, pinned, names: [name], kind });
|
|
2253
|
+
installs.push({ target: gov.tilde(t.dest), scope: t.scope, ide: t.ide, kind, name, version: lver(e) || null,
|
|
2254
|
+
source: gov.tilde(src), commit: (e && e.commit) || null, pinned,
|
|
2255
|
+
edited: kind === "agent" ? 0 : editedFiles(t.dest, name, e).length,
|
|
2256
|
+
policy: policy ? (refused.length ? refused.map((x) => x.why) : "ok") : null });
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
const report = { inventory: 1, created: new Date().toISOString(), label: args.flags.label && args.flags.label !== true ? String(args.flags.label) : null,
|
|
2260
|
+
cli: readJSON(path.join(ROOT, "package.json")).version, policy: policy ? policy.hash : null, installs };
|
|
2261
|
+
if (args.flags.json) return emitJSON(report);
|
|
2262
|
+
if (!installs.length) return console.log("no skilldrop installs found on this machine (looked in every --ide target, user and project scope)");
|
|
2263
|
+
for (const i of installs)
|
|
2264
|
+
console.log(`${i.target} ${i.kind} ${i.name}@${i.version || "-"} ${i.source}${i.pinned ? " (pinned)" : ""}` +
|
|
2265
|
+
(i.edited ? ` ${i.edited} edited file(s)` : "") + (Array.isArray(i.policy) ? ` REFUSED: ${i.policy[0]}` : ""));
|
|
2266
|
+
const bad = installs.filter((i) => Array.isArray(i.policy));
|
|
2267
|
+
console.log(`\n${installs.length} install(s)` + (policy ? `, ${bad.length} the policy refuses` : "") +
|
|
2268
|
+
". For an organisation-wide view: skilldrop inventory --json --label <machine> > <file>, then skilldrop inventory --merge <files...>");
|
|
2269
|
+
if (bad.length) process.exitCode = 1;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
function mergeInventories(files, flags) {
|
|
2273
|
+
const by = {};
|
|
2274
|
+
let reports = 0;
|
|
2275
|
+
for (const f of files) {
|
|
2276
|
+
let r;
|
|
2277
|
+
try { r = readJSON(f); } catch (e) { die(`${f} is not an inventory report`); }
|
|
2278
|
+
if (r.inventory !== 1 || !Array.isArray(r.installs)) die(`${f} is not an inventory report (skilldrop inventory --json)`);
|
|
2279
|
+
reports++;
|
|
2280
|
+
const who = r.label || path.basename(f);
|
|
2281
|
+
for (const i of r.installs) {
|
|
2282
|
+
const k = `${i.kind}\u0000${i.name}\u0000${i.source}`;
|
|
2283
|
+
const a = (by[k] = by[k] || { kind: i.kind, name: i.name, source: i.source, machines: new Set(), versions: new Set(), unpinned: 0, edited: 0, refused: 0 });
|
|
2284
|
+
a.machines.add(who);
|
|
2285
|
+
a.versions.add(i.version || "-");
|
|
2286
|
+
if (i.source !== "bundled" && !i.pinned) a.unpinned++;
|
|
2287
|
+
if (i.edited) a.edited++;
|
|
2288
|
+
if (Array.isArray(i.policy)) a.refused++;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const rows = Object.values(by).map((a) => Object.assign(a, { machines: [...a.machines].sort(), versions: [...a.versions].sort() }))
|
|
2292
|
+
.sort((x, y) => x.name.localeCompare(y.name));
|
|
2293
|
+
if (flags.json) return emitJSON({ reports, units: rows });
|
|
2294
|
+
for (const a of rows)
|
|
2295
|
+
console.log(`${a.kind} ${a.name} ${a.source} on ${a.machines.length} machine(s) version(s) ${a.versions.join(", ")}` +
|
|
2296
|
+
(a.unpinned ? ` ${a.unpinned} unpinned` : "") + (a.edited ? ` ${a.edited} edited` : "") + (a.refused ? ` ${a.refused} REFUSED by policy` : ""));
|
|
2297
|
+
console.log(`\n${rows.length} distinct install(s) across ${reports} report(s).`);
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/* skilldrop audit verify|show [<file>] — the append-only log install, update and uninstall
|
|
2301
|
+
write when a policy turns it on (or SKILLDROP_AUDIT_LOG names a file). */
|
|
2302
|
+
function auditCmd(args) {
|
|
2303
|
+
const sub = args._.shift();
|
|
2304
|
+
const policy = gov.load(args.flags);
|
|
2305
|
+
const file = args._[0] || process.env.SKILLDROP_AUDIT_LOG || (policy && policy.audit && (policy.audit.log || "~/.config/skilldrop/audit.jsonl"));
|
|
2306
|
+
if (!file) die("no audit log: pass a file, set SKILLDROP_AUDIT_LOG, or turn it on in the policy (audit.log)");
|
|
2307
|
+
if (sub === "verify") {
|
|
2308
|
+
const r = gov.verifyAudit(file);
|
|
2309
|
+
if (r.ok) return console.log(`ok: ${r.count} entr${r.count === 1 ? "y" : "ies"}, each following the one before it`);
|
|
2310
|
+
console.log(`BROKEN at line ${r.line} of ${r.count}: ${r.why}`);
|
|
2311
|
+
process.exitCode = 1;
|
|
2312
|
+
} else if (sub === "show") {
|
|
2313
|
+
const lines = fs.readFileSync(path.resolve(file.replace(/^~(?=\/)/, os.homedir())), "utf8").split("\n").filter(Boolean);
|
|
2314
|
+
const n = args.flags.last && args.flags.last !== true ? Math.max(1, parseInt(args.flags.last, 10) || 20) : 20;
|
|
2315
|
+
for (const line of lines.slice(-n)) {
|
|
2316
|
+
const e = JSON.parse(line);
|
|
2317
|
+
console.log(`${e.ts} ${e.action} ${e.kind} ${e.name}${e.version ? "@" + e.version : ""} ${e.source || ""}${e.pinned ? " (pinned)" : ""} ${e.target || ""}${e.actor ? " by " + e.actor : ""}`);
|
|
2318
|
+
}
|
|
2319
|
+
} else die("usage: skilldrop audit verify|show [<file>] [--last N]");
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
/* skilldrop verify-mirror <dir> — is an internal mirror still what skilldrop package made? */
|
|
2323
|
+
function verifyMirrorCmd(args) {
|
|
2324
|
+
const r = gov.verifyMirror(args._[0] || die("usage: skilldrop verify-mirror <dir>"));
|
|
2325
|
+
if (args.flags.json) { emitJSON(r); if (r.missing.length || r.changed.length || r.extra.length) process.exitCode = 1; return; }
|
|
2326
|
+
for (const f of r.changed) console.log(`changed ${f}`);
|
|
2327
|
+
for (const f of r.missing) console.log(`missing ${f}`);
|
|
2328
|
+
for (const f of r.extra) console.log(`added ${f} (not in MIRROR.json)`);
|
|
2329
|
+
const bad = r.changed.length + r.missing.length + r.extra.length;
|
|
2330
|
+
console.log(bad ? `\n${bad} file(s) differ from what was packaged from '${r.source}' on ${r.created}.`
|
|
2331
|
+
: `ok: all ${r.files} file(s) match what was packaged from '${r.source}' on ${r.created}.`);
|
|
2332
|
+
if (bad) process.exitCode = 1;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2152
2335
|
function bootstrap() {
|
|
2153
2336
|
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
2154
2337
|
let data = {};
|
|
@@ -2178,7 +2361,8 @@ function bootstrap() {
|
|
|
2178
2361
|
|
|
2179
2362
|
const args = parseArgs(process.argv.slice(2));
|
|
2180
2363
|
const cmd = args._.shift();
|
|
2181
|
-
const commands = { list, info, packs: listPacks, profiles: listProfiles, agents: listAgents, loops: listLoops, install, update, outdated, uninstall, diff, doctor, "new-skill": newSkill, "loop-stats": loopStats, "init-catalogue": initCatalogue, package: packageCatalogue, validate: validateCmd, scan, bootstrap
|
|
2364
|
+
const commands = { list, info, packs: listPacks, profiles: listProfiles, agents: listAgents, loops: listLoops, install, update, outdated, uninstall, diff, doctor, "new-skill": newSkill, "loop-stats": loopStats, "init-catalogue": initCatalogue, package: packageCatalogue, validate: validateCmd, scan, bootstrap,
|
|
2365
|
+
inventory, audit: auditCmd, "verify-mirror": verifyMirrorCmd };
|
|
2182
2366
|
if (args.flags.version || cmd === "version") console.log(readJSON(path.join(ROOT, "package.json")).version);
|
|
2183
2367
|
else if (!cmd || cmd === "help" || args.flags.help) console.log(HELP);
|
|
2184
2368
|
else if (commands[cmd]) commands[cmd](args);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"note": "Example: Platform Security owns this policy. Ask in #ai-tools before adding a catalogue.",
|
|
4
|
+
"catalogues": {
|
|
5
|
+
"allow": ["bundled", "https://git.example.org/ai/skills"],
|
|
6
|
+
"require_pinned": true
|
|
7
|
+
},
|
|
8
|
+
"skills": {
|
|
9
|
+
"allow": ["*"],
|
|
10
|
+
"block": ["figma-diagrams"]
|
|
11
|
+
},
|
|
12
|
+
"scan": { "max": "medium" },
|
|
13
|
+
"audit": { "actor": "pseudonym", "salt_env": "SKILLDROP_AUDIT_SALT" }
|
|
14
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/sananthanarayan/skilldrop/contracts/policy.schema.json",
|
|
4
|
+
"title": "skilldrop organisation policy",
|
|
5
|
+
"$comment": "RFC-0050. The file an organisation hands out (--policy, SKILLDROP_POLICY, or ~/.config/skilldrop/policy.json). Closed at every level: an unknown key stops the command, because a misspelt rule that is silently ignored is a rule that isn't there. bin/governance.js enforces the same shape; contracts/policy.example.json must satisfy both.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": { "const": 1 },
|
|
11
|
+
"note": { "type": "string", "description": "Free text: who owns this policy and where to ask about it." },
|
|
12
|
+
"catalogues": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"properties": {
|
|
16
|
+
"allow": { "type": "array", "items": { "type": "string" }, "description": "Catalogue sources allowed: \"bundled\", a git URL (a trailing * matches a prefix, e.g. https://git.example.org/skills/*), or a path. Absent: any." },
|
|
17
|
+
"require_pinned": { "type": "boolean", "description": "Every catalogue other than the bundled one must be pinned to a commit (--from <url>#<commit>)." }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"skills": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"additionalProperties": false,
|
|
23
|
+
"properties": {
|
|
24
|
+
"allow": { "type": "array", "items": { "type": "string" }, "description": "Skill, loop and agent names allowed; * matches anything. Absent: any." },
|
|
25
|
+
"block": { "type": "array", "items": { "type": "string" }, "description": "Names refused even when allowed." }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scan": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"properties": {
|
|
32
|
+
"max": { "enum": ["none", "low", "medium", "high"], "description": "The worst supply-chain scan finding a skill may carry: none, low (🟨), medium (🟧), high (🟥). Findings in eval fixtures and example inputs never count." }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"audit": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"description": "Present: install, update and uninstall append to a tamper-evident log.",
|
|
39
|
+
"properties": {
|
|
40
|
+
"log": { "type": "string", "description": "Where the log goes. Default ~/.config/skilldrop/audit.jsonl. Refused inside a git working tree." },
|
|
41
|
+
"actor": { "description": "Who each entry names: \"pseudonym\" (default; a keyed hash of user and machine, needs the salt), \"none\", \"os-user\", or {\"fixed\": \"<name>\"} such as a team or machine id." },
|
|
42
|
+
"salt_env": { "type": "string", "description": "The environment variable holding the organisation's secret salt for pseudonyms. Default SKILLDROP_AUDIT_SALT." }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilldrop-cli",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.27",
|
|
4
4
|
"description": "Portable AI-agent skills, measured against the agent without them. ADRs, PRDs, runbooks, decks and reviews, installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sanjay Ananthanarayan",
|
|
@@ -99,10 +99,9 @@ Two things follow:
|
|
|
99
99
|
right lines or mark the item assumed. Add `--mermaid diagram.mmd` for a version to paste
|
|
100
100
|
into a README, and `--compare old.json` to show what a change adds, removes and alters.
|
|
101
101
|
|
|
102
|
-
5. **
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
were checked:
|
|
102
|
+
5. **Lead the reply with the map.** The script prints a titled link to the page. Put it at the
|
|
103
|
+
top, then the takeaway in one or two sentences, then one line each for what is drawn as an
|
|
104
|
+
assumption and what was left out, and last how many citations were checked:
|
|
106
105
|
|
|
107
106
|
> **What a checkout request touches** (the script's link line: the title, linked to the page)
|
|
108
107
|
>
|
|
@@ -110,8 +109,8 @@ Two things follow:
|
|
|
110
109
|
> Assumed: the shopper's browser (no client in this repo). Left out: the products table's
|
|
111
110
|
> other readers. 22 of 22 citations checked.
|
|
112
111
|
|
|
113
|
-
Keep the spec beside the page so the map can be rebuilt after the code changes.
|
|
114
|
-
|
|
112
|
+
Keep the spec beside the page so the map can be rebuilt after the code changes. At most one
|
|
113
|
+
short line about the build may come before the link; no recap of commands or layout.
|
|
115
114
|
|
|
116
115
|
6. **When the script can't run** (no `python3`), give the Mermaid version in the reply, with
|
|
117
116
|
assumptions marked, and say that the interactive page needs Python 3.
|
|
@@ -131,7 +130,8 @@ don't cite them in the first place, and read the page before it leaves the team.
|
|
|
131
130
|
- **Nothing on the map comes only from a README, a plan or what such systems usually have.**
|
|
132
131
|
- **The map answers one stated question,** and what was left out is said.
|
|
133
132
|
- **One HTML file that works offline:** no CDN, no external fonts, no network requests.
|
|
134
|
-
- **The
|
|
133
|
+
- **The link and the takeaway lead the reply,** with at most one short line about the build
|
|
134
|
+
before them and no account of how it was made.
|
|
135
135
|
|
|
136
136
|
## When to use this skill
|
|
137
137
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "interactive-diagram",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Turn a codebase, a system description, a process or a database schema into one self-contained interactive HTML diagram (architecture, workflow, data flow, lifecycle, ERD, sequence, class, tree, timeline or waterfall) where every box and arrow cites the file and line or the words it came from, anything inferred is drawn dashed as an assumption, and each cited line is checked against the repository before the page is written. Laid out automatically, opens offline with no CDN, and exports SVG, PNG and Mermaid. Use when the user asks to \"visualize\", \"map\" or \"make an interactive diagram of\" a repo, a request path, a pipeline, an approval process, a set of tables, an incident's events or a slow trace, or wants a diagram they can click through and share as one file.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -2591,7 +2591,7 @@ def main(argv=None):
|
|
|
2591
2591
|
else:
|
|
2592
2592
|
item, link = {"sequence": ("participants", "messages"), "tree": ("items", "links"), "timeline": ("events", "links"),
|
|
2593
2593
|
"waterfall": ("spans", "parent links"), "class": ("types", "relations")}.get(spec["type"], ("nodes", "edges"))
|
|
2594
|
-
print("
|
|
2594
|
+
print("Lead the reply with this line, then the takeaway:\n" + receipt["reply_first_line"] + "\n")
|
|
2595
2595
|
print("For the end of the reply: %d of %d file citations checked; %d drawn as assumptions. (%s, %d KB: %d %s, %d %s.)" % (
|
|
2596
2596
|
receipt["verified"], receipt["file_citations"], receipt["assumed"], a.out, receipt["bytes"] // 1024,
|
|
2597
2597
|
receipt["nodes"], item, receipt["edges"], link))
|