agent-skills-standard 2.6.0 → 2.6.3
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/LICENSE +21 -201
- package/README.md +2 -2
- package/dist/index.js +1120 -210
- package/dist/index.js.map +1 -1
- package/package.json +19 -10
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
20
|
value: mod,
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
@@ -26,6 +26,7 @@ let dotenv = require("dotenv");
|
|
|
26
26
|
dotenv = __toESM(dotenv);
|
|
27
27
|
let picocolors = require("picocolors");
|
|
28
28
|
picocolors = __toESM(picocolors);
|
|
29
|
+
let node_crypto = require("node:crypto");
|
|
29
30
|
let fs_extra = require("fs-extra");
|
|
30
31
|
fs_extra = __toESM(fs_extra);
|
|
31
32
|
let path = require("path");
|
|
@@ -43,7 +44,7 @@ let child_process = require("child_process");
|
|
|
43
44
|
let fs = require("fs");
|
|
44
45
|
var package_default = {
|
|
45
46
|
name: "agent-skills-standard",
|
|
46
|
-
version: "2.6.
|
|
47
|
+
version: "2.6.3",
|
|
47
48
|
description: "A CLI to manage and sync AI agent skills standard for Cursor, Claude, Copilot, Windsurf, and more.",
|
|
48
49
|
main: "dist/index.js",
|
|
49
50
|
bin: {
|
|
@@ -77,39 +78,121 @@ var package_default = {
|
|
|
77
78
|
],
|
|
78
79
|
author: "Hoang Nguyen",
|
|
79
80
|
license: "MIT",
|
|
81
|
+
repository: {
|
|
82
|
+
"type": "git",
|
|
83
|
+
"url": "https://github.com/HoangNguyen0403/agent-skills-standard",
|
|
84
|
+
"directory": "cli"
|
|
85
|
+
},
|
|
86
|
+
bugs: { "url": "https://github.com/HoangNguyen0403/agent-skills-standard/issues" },
|
|
87
|
+
homepage: "https://github.com/HoangNguyen0403/agent-skills-standard#readme",
|
|
80
88
|
dependencies: {
|
|
81
|
-
"commander": "^
|
|
89
|
+
"commander": "^15.0.0",
|
|
82
90
|
"cross-fetch": "^4.1.0",
|
|
83
|
-
"dotenv": "^17.2
|
|
84
|
-
"fs-extra": "^11.
|
|
85
|
-
"inquirer": "^
|
|
86
|
-
"js-yaml": "^4.2
|
|
91
|
+
"dotenv": "^17.4.2",
|
|
92
|
+
"fs-extra": "^11.4.0",
|
|
93
|
+
"inquirer": "^14.2.2",
|
|
94
|
+
"js-yaml": "^5.4.2",
|
|
87
95
|
"picocolors": "^1.0.0",
|
|
88
|
-
"zod": "^
|
|
96
|
+
"zod": "^4.6.5"
|
|
89
97
|
},
|
|
90
98
|
devDependencies: {
|
|
91
|
-
"@vitest/coverage-v8": "^
|
|
92
|
-
"vite": "^
|
|
93
|
-
"vitest": "^
|
|
99
|
+
"@vitest/coverage-v8": "^5.0.1",
|
|
100
|
+
"vite": "^8.3.0",
|
|
101
|
+
"vitest": "^5.0.1"
|
|
94
102
|
}
|
|
95
103
|
};
|
|
96
104
|
//#endregion
|
|
97
|
-
//#region src/services/
|
|
98
|
-
|
|
105
|
+
//#region src/services/assertion-semantics.ts
|
|
106
|
+
var SEMANTIC_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
107
|
+
"a",
|
|
108
|
+
"an",
|
|
109
|
+
"and",
|
|
110
|
+
"as",
|
|
111
|
+
"at",
|
|
112
|
+
"by",
|
|
113
|
+
"for",
|
|
114
|
+
"from",
|
|
115
|
+
"in",
|
|
116
|
+
"into",
|
|
117
|
+
"is",
|
|
118
|
+
"it",
|
|
119
|
+
"of",
|
|
120
|
+
"on",
|
|
121
|
+
"or",
|
|
122
|
+
"the",
|
|
123
|
+
"to",
|
|
124
|
+
"use",
|
|
125
|
+
"via",
|
|
126
|
+
"with"
|
|
127
|
+
]);
|
|
128
|
+
function normalizedText(value) {
|
|
129
|
+
return value.toLowerCase().replace(/[`*_~]/g, "").replace(/[’]/g, "'").replace(/\s+/g, " ").trim();
|
|
130
|
+
}
|
|
131
|
+
function compactText(value) {
|
|
132
|
+
return normalizedText(value).replace(/\s+/g, "");
|
|
133
|
+
}
|
|
134
|
+
function semanticTokens(value) {
|
|
135
|
+
return (normalizedText(value).replace(/[-_/]/g, " ").match(/@[a-z][a-z0-9]*|[a-z][a-z0-9]*|\d+/g) ?? []).filter((token) => !SEMANTIC_STOP_WORDS.has(token) && token.length > 2);
|
|
136
|
+
}
|
|
137
|
+
function tokenVariants(value) {
|
|
138
|
+
const variants = /* @__PURE__ */ new Set([value]);
|
|
139
|
+
const stemmed = value.replace(/(ing|ed|es|s)$/, "");
|
|
140
|
+
variants.add(stemmed);
|
|
141
|
+
if (value.endsWith("ing")) variants.add(`${stemmed}e`);
|
|
142
|
+
if (stemmed.endsWith("e")) variants.add(stemmed.slice(0, -1));
|
|
143
|
+
return variants;
|
|
144
|
+
}
|
|
145
|
+
function containsV2(value, transcript) {
|
|
146
|
+
const needle = normalizedText(value);
|
|
147
|
+
if (normalizedText(transcript).includes(needle)) return true;
|
|
148
|
+
if (/[{}[\]@/:?<>$%()]/.test(value)) {
|
|
149
|
+
if (compactText(transcript).includes(compactText(value))) return true;
|
|
150
|
+
}
|
|
151
|
+
if (/\d/.test(value)) return false;
|
|
152
|
+
if (/[{}[\]@/:?<>$%()]/.test(value)) {
|
|
153
|
+
const genericFunction = value.match(/^([a-z_$][a-z0-9_$]*)\s*<[^>]+>\s*\(\s*\)$/i);
|
|
154
|
+
if (genericFunction) return new RegExp(`${genericFunction[1]}\\s*(?:<[^>]+>)?\\s*\\(`, "i").test(transcript);
|
|
155
|
+
const constructorShape = value.match(/\b([A-Z][A-Za-z0-9_]*)\s*\(\s*val\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
|
156
|
+
if (constructorShape) return new RegExp(`${constructorShape[1]}\\s*\\([\\s\\S]*?${constructorShape[2]}`, "i").test(transcript);
|
|
157
|
+
if (/^@for\s*\(/i.test(value)) return /@for\s*\(/i.test(transcript) && /\btrack\b/i.test(transcript) && /(?:\.\s*id|\bid\b)/i.test(transcript);
|
|
158
|
+
if (/^@if\s*\(/i.test(value)) return /@if\s*\(/i.test(transcript);
|
|
159
|
+
if (/^@empty\b/i.test(value)) return /@empty\b/i.test(transcript);
|
|
160
|
+
const functionName = value.match(/^([a-z_$][a-z0-9_$]*)\(\s*\)$/i)?.[1];
|
|
161
|
+
if (functionName) return new RegExp(`${functionName}\\s*\\(`, "i").test(transcript);
|
|
162
|
+
}
|
|
163
|
+
const required = semanticTokens(value);
|
|
164
|
+
const available = new Set(semanticTokens(transcript).flatMap((token) => [...tokenVariants(token)]));
|
|
165
|
+
if (required.length === 1) return [...tokenVariants(required[0])].some((variant) => available.has(variant));
|
|
166
|
+
if (required.length === 0) return false;
|
|
167
|
+
return required.every((token) => {
|
|
168
|
+
return [...tokenVariants(token)].some((variant) => available.has(variant) || [...available].some((candidate) => candidate.startsWith(variant) || variant.startsWith(candidate)));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Basename without importing `path`, so the module stays runtime-agnostic. */
|
|
172
|
+
function basename(value) {
|
|
173
|
+
const parts = value.split(/[\\/]/);
|
|
174
|
+
return parts[parts.length - 1] ?? value;
|
|
175
|
+
}
|
|
176
|
+
function checkAssertion(assertion, transcript, semanticsVersion = 1) {
|
|
99
177
|
const haystack = transcript.toLowerCase();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
178
|
+
switch (assertion.type) {
|
|
179
|
+
case "contains": return semanticsVersion === 2 ? containsV2(String(assertion.value), transcript) : haystack.includes(String(assertion.value).toLowerCase());
|
|
180
|
+
case "contains_any": return (Array.isArray(assertion.value) ? assertion.value : [assertion.value]).some((value) => semanticsVersion === 2 ? containsV2(String(value), transcript) : haystack.includes(String(value).toLowerCase()));
|
|
181
|
+
case "not_contains": return !haystack.includes(String(assertion.value).toLowerCase());
|
|
182
|
+
case "regex": try {
|
|
183
|
+
return new RegExp(String(assertion.value), "i").test(transcript);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
case "file_reference": {
|
|
188
|
+
const value = String(assertion.value).toLowerCase();
|
|
189
|
+
return haystack.includes(value) || haystack.includes(basename(value));
|
|
190
|
+
}
|
|
191
|
+
default: return false;
|
|
110
192
|
}
|
|
111
|
-
return haystack.includes(String(assertion.value).toLowerCase());
|
|
112
193
|
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/services/EvalsVerifier.ts
|
|
113
196
|
function answerPath(runDir, manifest, skill, caseId, arm) {
|
|
114
197
|
const categoryParts = manifest.category === "all" ? [skill.category] : [];
|
|
115
198
|
const filename = arm ? `${caseId}.${arm}.md` : `${caseId}.md`;
|
|
@@ -123,6 +206,75 @@ function readInputs(runDir) {
|
|
|
123
206
|
const inputsPath = path.join(runDir, "inputs.json");
|
|
124
207
|
return fs_extra.default.existsSync(inputsPath) ? fs_extra.default.readJSONSync(inputsPath) : null;
|
|
125
208
|
}
|
|
209
|
+
function isStringRecord(value) {
|
|
210
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
211
|
+
}
|
|
212
|
+
function isBase64(value) {
|
|
213
|
+
return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
|
|
214
|
+
}
|
|
215
|
+
function resourceFingerprintDiffs(manifest, inputs) {
|
|
216
|
+
if (manifest.schemaVersion !== 2 || manifest.resourceFingerprints === void 0) return [];
|
|
217
|
+
if (typeof manifest.resourceFingerprints !== "object" || manifest.resourceFingerprints === null || Array.isArray(manifest.resourceFingerprints)) return ["Invalid immutable resource fingerprint manifest"];
|
|
218
|
+
const diffs = [];
|
|
219
|
+
for (const key of manifest.skills.map((skill) => `${skill.category}/${skill.skillName}`).sort()) if (!Object.hasOwn(manifest.resourceFingerprints, key)) diffs.push(`Missing immutable resource provenance for ${key}`);
|
|
220
|
+
for (const key of Object.keys(manifest.resourceFingerprints).sort()) {
|
|
221
|
+
const fingerprint = manifest.resourceFingerprints[key];
|
|
222
|
+
if (!fingerprint || fingerprint.version !== 1 || !isStringRecord(fingerprint.resources)) {
|
|
223
|
+
diffs.push(`Invalid immutable resource fingerprint for ${key}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const resources = inputs?.sources[key]?.resources;
|
|
227
|
+
if (!isStringRecord(resources)) {
|
|
228
|
+
diffs.push(`Missing immutable resource provenance for ${key}`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const expectedPaths = Object.keys(fingerprint.resources).sort();
|
|
232
|
+
const actualPaths = Object.keys(resources).sort();
|
|
233
|
+
for (const resourcePath of expectedPaths) {
|
|
234
|
+
const content = resources[resourcePath];
|
|
235
|
+
if (content === void 0) {
|
|
236
|
+
diffs.push(`Missing immutable resource provenance for ${key}/${resourcePath}`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!isBase64(content) || (0, node_crypto.createHash)("sha256").update(Buffer.from(content, "base64")).digest("hex") !== fingerprint.resources[resourcePath]) diffs.push(`Immutable resource mismatch for ${key}/${resourcePath}`);
|
|
240
|
+
}
|
|
241
|
+
for (const resourcePath of actualPaths) if (!(resourcePath in fingerprint.resources)) diffs.push(`Immutable resource mismatch for ${key}/${resourcePath}`);
|
|
242
|
+
}
|
|
243
|
+
return diffs;
|
|
244
|
+
}
|
|
245
|
+
function rawSourceDiffs(manifest, inputs) {
|
|
246
|
+
if (manifest.schemaVersion !== 2 || manifest.inputProvenanceVersion !== 1) return [];
|
|
247
|
+
const diffs = [];
|
|
248
|
+
for (const skill of manifest.skills) {
|
|
249
|
+
const key = `${skill.category}/${skill.skillName}`;
|
|
250
|
+
const expected = manifest.sourceHashes?.[key];
|
|
251
|
+
const source = inputs?.sources[key];
|
|
252
|
+
if (!expected || !source) {
|
|
253
|
+
diffs.push(`Missing immutable raw source provenance for ${key}`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const skillBase64 = source.skillMarkdownBase64;
|
|
257
|
+
if (skillBase64 === void 0 || !isBase64(skillBase64)) diffs.push(`Missing immutable raw skill provenance for ${key}`);
|
|
258
|
+
else {
|
|
259
|
+
const skillBytes = Buffer.from(skillBase64, "base64");
|
|
260
|
+
if ((0, node_crypto.createHash)("sha256").update(skillBytes).digest("hex") !== expected.skill) diffs.push(`Raw skill snapshot hash mismatch for ${key}`);
|
|
261
|
+
if (source.skillMarkdown !== skillBytes.toString("utf8")) diffs.push(`Raw skill snapshot mismatch for ${key}`);
|
|
262
|
+
}
|
|
263
|
+
const evalsBase64 = source.evalsBase64;
|
|
264
|
+
if (evalsBase64 === void 0 || !isBase64(evalsBase64)) {
|
|
265
|
+
diffs.push(`Missing immutable raw eval provenance for ${key}`);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const evalsBytes = Buffer.from(evalsBase64, "base64");
|
|
269
|
+
if ((0, node_crypto.createHash)("sha256").update(evalsBytes).digest("hex") !== expected.evals) diffs.push(`Raw eval snapshot hash mismatch for ${key}`);
|
|
270
|
+
try {
|
|
271
|
+
if (JSON.stringify(JSON.parse(evalsBytes.toString("utf8"))) !== JSON.stringify(source.evals)) diffs.push(`Raw eval snapshot mismatch for ${key}`);
|
|
272
|
+
} catch {
|
|
273
|
+
diffs.push(`Invalid immutable raw eval JSON for ${key}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return diffs;
|
|
277
|
+
}
|
|
126
278
|
function evalDataFor(projectRoot, runDir, skill) {
|
|
127
279
|
const inputs = readInputs(runDir);
|
|
128
280
|
if (inputs) {
|
|
@@ -147,6 +299,7 @@ function summarizeSkill(projectRoot, runDir, manifest, skill) {
|
|
|
147
299
|
const evalsData = evalDataFor(projectRoot, runDir, skill);
|
|
148
300
|
const evalById = new Map((evalsData.evals ?? []).map((evaluation) => [String(evaluation.id), evaluation]));
|
|
149
301
|
const pressureByIndex = evalsData.pressure_scenarios ?? [];
|
|
302
|
+
const semanticsVersion = manifest.schemaVersion === 2 ? manifest.provenance?.[`${skill.category}/${skill.skillName}`]?.assertionSemanticsVersion ?? manifest.assertionSemanticsVersion ?? 1 : 1;
|
|
150
303
|
const baseline = [];
|
|
151
304
|
const withSkill = [];
|
|
152
305
|
const baselineAssertions = [];
|
|
@@ -170,7 +323,7 @@ function summarizeSkill(projectRoot, runDir, manifest, skill) {
|
|
|
170
323
|
if (!(arm in currentCase.arms)) continue;
|
|
171
324
|
const transcript = readAnswer(runDir, manifest, skill, currentCase, arm);
|
|
172
325
|
if (transcript === null) throw new Error(`missing answer: ${skill.skillName}/${currentCase.id}.${arm}`);
|
|
173
|
-
const checks = assertions.map((assertion) => checkAssertion(assertion, transcript));
|
|
326
|
+
const checks = assertions.map((assertion) => checkAssertion(assertion, transcript, semanticsVersion));
|
|
174
327
|
const passed = checks.every(Boolean);
|
|
175
328
|
(arm === "baseline" ? baseline : withSkill).push(passed);
|
|
176
329
|
(arm === "baseline" ? baselineAssertions : withSkillAssertions).push({
|
|
@@ -220,11 +373,19 @@ function verifyEvalRun(projectRoot, runId) {
|
|
|
220
373
|
reason: "run is missing manifest.json or results.json"
|
|
221
374
|
};
|
|
222
375
|
const manifest = fs_extra.default.readJSONSync(manifestPath);
|
|
223
|
-
|
|
376
|
+
const inputs = readInputs(runDir);
|
|
377
|
+
if (manifest.schemaVersion === 2 && inputs === null) return {
|
|
224
378
|
runId,
|
|
225
379
|
ok: false,
|
|
226
380
|
reason: "v2 run is missing immutable inputs.json"
|
|
227
381
|
};
|
|
382
|
+
const provenanceDiffs = [...resourceFingerprintDiffs(manifest, inputs), ...rawSourceDiffs(manifest, inputs)];
|
|
383
|
+
if (provenanceDiffs.length > 0) return {
|
|
384
|
+
runId,
|
|
385
|
+
ok: false,
|
|
386
|
+
reason: "immutable source provenance differs from manifest",
|
|
387
|
+
diffs: provenanceDiffs
|
|
388
|
+
};
|
|
228
389
|
const committed = fs_extra.default.readJSONSync(resultsPath);
|
|
229
390
|
const diffs = [];
|
|
230
391
|
for (const skill of manifest.skills) {
|
|
@@ -450,7 +611,7 @@ var FeedbackCommand = class {
|
|
|
450
611
|
when: !loadedSkills
|
|
451
612
|
},
|
|
452
613
|
{
|
|
453
|
-
type: "
|
|
614
|
+
type: "rawlist",
|
|
454
615
|
name: "rootCause",
|
|
455
616
|
message: "Root cause of the violation (optional)",
|
|
456
617
|
when: !rootCause,
|
|
@@ -934,8 +1095,7 @@ var COMMON_SKILL_EXCLUDES = {
|
|
|
934
1095
|
mobile: [
|
|
935
1096
|
"common-accessibility",
|
|
936
1097
|
"common-api-design",
|
|
937
|
-
"common-observability"
|
|
938
|
-
"common-system-design"
|
|
1098
|
+
"common-observability"
|
|
939
1099
|
]
|
|
940
1100
|
};
|
|
941
1101
|
/**
|
|
@@ -954,6 +1114,8 @@ var DEFAULT_WORKFLOWS = [
|
|
|
954
1114
|
"brainstorm-feature",
|
|
955
1115
|
"code-review",
|
|
956
1116
|
"codebase-review",
|
|
1117
|
+
"system-design-session",
|
|
1118
|
+
"review-system-design",
|
|
957
1119
|
"design-solution",
|
|
958
1120
|
"deploy-release",
|
|
959
1121
|
"plan-feature",
|
|
@@ -969,9 +1131,11 @@ var DEFAULT_WORKFLOWS = [
|
|
|
969
1131
|
"implement-feature",
|
|
970
1132
|
"verify-work",
|
|
971
1133
|
"verify-bug",
|
|
1134
|
+
"test-loop",
|
|
972
1135
|
"security-test",
|
|
973
1136
|
"uat-signoff",
|
|
974
|
-
"incident-hotfix"
|
|
1137
|
+
"incident-hotfix",
|
|
1138
|
+
"monitor-respond"
|
|
975
1139
|
];
|
|
976
1140
|
/**
|
|
977
1141
|
* Workflows that must never be synced to a consumer project, regardless of
|
|
@@ -1288,7 +1452,8 @@ var SkillConfigSchema = zod.z.object({
|
|
|
1288
1452
|
})),
|
|
1289
1453
|
custom_overrides: zod.z.array(zod.z.string()).optional(),
|
|
1290
1454
|
workflows: zod.z.union([zod.z.boolean(), zod.z.array(zod.z.string())]).optional(),
|
|
1291
|
-
mcp: McpConfigSchema.optional()
|
|
1455
|
+
mcp: McpConfigSchema.optional(),
|
|
1456
|
+
telemetry: zod.z.boolean().optional()
|
|
1292
1457
|
});
|
|
1293
1458
|
var DEFAULT_SDLC_SUPPORT_CATEGORIES = ["quality-engineering"];
|
|
1294
1459
|
var PRIMARY_CONFIG_FILE = ".skillsrc";
|
|
@@ -1312,7 +1477,7 @@ var ConfigService = class {
|
|
|
1312
1477
|
return null;
|
|
1313
1478
|
}
|
|
1314
1479
|
dumpConfig(config) {
|
|
1315
|
-
return js_yaml.
|
|
1480
|
+
return js_yaml.dump(config, {
|
|
1316
1481
|
noRefs: true,
|
|
1317
1482
|
lineWidth: -1
|
|
1318
1483
|
});
|
|
@@ -1357,7 +1522,7 @@ var ConfigService = class {
|
|
|
1357
1522
|
agents.push(match[1].trim());
|
|
1358
1523
|
}
|
|
1359
1524
|
config.agents = agents;
|
|
1360
|
-
const sectionNames = new Set([
|
|
1525
|
+
const sectionNames = /* @__PURE__ */ new Set([
|
|
1361
1526
|
"custom_overrides",
|
|
1362
1527
|
"workflows",
|
|
1363
1528
|
"mcp"
|
|
@@ -1487,7 +1652,7 @@ var ConfigService = class {
|
|
|
1487
1652
|
let rawConfig;
|
|
1488
1653
|
let migratedFromText = false;
|
|
1489
1654
|
try {
|
|
1490
|
-
rawConfig = js_yaml.
|
|
1655
|
+
rawConfig = js_yaml.load(content);
|
|
1491
1656
|
} catch (parseError) {
|
|
1492
1657
|
rawConfig = this.migrateLegacyTextConfig(content);
|
|
1493
1658
|
migratedFromText = rawConfig !== null;
|
|
@@ -1500,7 +1665,7 @@ var ConfigService = class {
|
|
|
1500
1665
|
if (shouldRenameLegacyYamlFile) await fs_extra.default.remove(configPath);
|
|
1501
1666
|
return parsed.data;
|
|
1502
1667
|
} catch (error) {
|
|
1503
|
-
throw new Error(`Failed to load config: ${error}
|
|
1668
|
+
throw new Error(`Failed to load config: ${error}`, { cause: error });
|
|
1504
1669
|
}
|
|
1505
1670
|
}
|
|
1506
1671
|
/**
|
|
@@ -1647,16 +1812,35 @@ var ConfigService = class {
|
|
|
1647
1812
|
//#endregion
|
|
1648
1813
|
//#region src/services/HookService.ts
|
|
1649
1814
|
/**
|
|
1815
|
+
* Version marker embedded as the hook script's first line after the shebang.
|
|
1816
|
+
* On install, an existing script carrying an *older* marker is understood to
|
|
1817
|
+
* be our own previous auto-generated version (not a user customization) and
|
|
1818
|
+
* is refreshed; a script with no marker at all predates this feature and is
|
|
1819
|
+
* left untouched, matching this hook's original "create once, preserve if
|
|
1820
|
+
* customized" contract. A script that keeps the marker line but edits the
|
|
1821
|
+
* body around it will still be treated as ours and overwritten on the next
|
|
1822
|
+
* version bump — that residual case can't be told apart from a stale file
|
|
1823
|
+
* without a full diff/backup mechanism, which is out of scope here.
|
|
1824
|
+
*/
|
|
1825
|
+
var HOOK_SCRIPT_VERSION = package_default.version;
|
|
1826
|
+
var HOOK_VERSION_MARKER_RE = /^\/\/ ags-hook-version: (\S+)$/m;
|
|
1827
|
+
/**
|
|
1650
1828
|
* Universal PreToolUse hook script (Node.js / JavaScript).
|
|
1651
1829
|
* Reminds AI agents to call load_skills_for_files() before any code edit.
|
|
1652
1830
|
* Guaranteed to execute on any machine running agent-skills-standard.
|
|
1653
1831
|
* Always exits 0 to never block work.
|
|
1654
1832
|
*/
|
|
1655
1833
|
var UNIVERSAL_SKILL_LOADER_JS = `#!/usr/bin/env node
|
|
1834
|
+
// ags-hook-version: ${HOOK_SCRIPT_VERSION}
|
|
1656
1835
|
/**
|
|
1657
1836
|
* PreToolUse hook: remind AI agent to call load_skills_for_files() before any code edit.
|
|
1658
1837
|
* Installed by agent-skills-standard (ags sync). Remove via: ags hooks uninstall
|
|
1659
|
-
* Guaranteed to execute on any system with Node.js. Always exits 0 to never block work
|
|
1838
|
+
* Guaranteed to execute on any system with Node.js. Always exits 0 to never block work,
|
|
1839
|
+
* UNLESS AGS_HOOK_ENFORCE=1 is set in the environment (opt-in via
|
|
1840
|
+
* \`ags hooks install --enforce\`) AND the target file matches the small,
|
|
1841
|
+
* fixed, skill-agnostic deny-list below — see installStandardHookConfig's
|
|
1842
|
+
* doc comment in HookService.ts for why this can't be a per-skill
|
|
1843
|
+
* permissions.filesystem.deny check.
|
|
1660
1844
|
*/
|
|
1661
1845
|
const fs = require('fs');
|
|
1662
1846
|
const path = require('path');
|
|
@@ -1693,6 +1877,19 @@ function shouldSkip(filePath) {
|
|
|
1693
1877
|
}
|
|
1694
1878
|
}
|
|
1695
1879
|
|
|
1880
|
+
const ENFORCE = process.env.AGS_HOOK_ENFORCE === '1';
|
|
1881
|
+
const DENY_BASENAMES = new Set(['SOUL.md', 'MEMORY.md']);
|
|
1882
|
+
const DENY_PATTERNS = [
|
|
1883
|
+
/(^|[\\\\/])\\.env(\\.[^\\\\/]*)?$/i,
|
|
1884
|
+
/(^|[\\\\/])\\.ssh([\\\\/]|$)/i,
|
|
1885
|
+
/credentials[^\\\\/]*\\.(json|ya?ml)$/i,
|
|
1886
|
+
];
|
|
1887
|
+
|
|
1888
|
+
function isDenied(filePath) {
|
|
1889
|
+
if (DENY_BASENAMES.has(path.basename(filePath))) return true;
|
|
1890
|
+
return DENY_PATTERNS.some(re => re.test(filePath));
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1696
1893
|
let input = '';
|
|
1697
1894
|
process.stdin.setEncoding('utf8');
|
|
1698
1895
|
process.stdin.on('data', chunk => { input += chunk; });
|
|
@@ -1704,6 +1901,15 @@ process.stdin.on('end', () => {
|
|
|
1704
1901
|
const filePath = data.tool_input?.file_path || '';
|
|
1705
1902
|
if (!filePath || shouldSkip(filePath)) process.exit(0);
|
|
1706
1903
|
|
|
1904
|
+
if (ENFORCE && isDenied(filePath)) {
|
|
1905
|
+
console.error(
|
|
1906
|
+
'[AGS BLOCKED] Refusing to edit ' + path.basename(filePath) + ': ' +
|
|
1907
|
+
'matches the default identity/secret deny-list (SOUL.md, MEMORY.md, .env*, .ssh/, credentials*.json|yaml).\\n' +
|
|
1908
|
+
'If this edit is intentional, re-run \`ags hooks install\` (without --enforce) to return to advisory-only mode.'
|
|
1909
|
+
);
|
|
1910
|
+
process.exit(2);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1707
1913
|
const fileName = path.basename(filePath);
|
|
1708
1914
|
console.log(
|
|
1709
1915
|
'[SKILL TRIGGER] Editing: ' + fileName + '\\n' +
|
|
@@ -1771,15 +1977,18 @@ var HookService = class {
|
|
|
1771
1977
|
continue;
|
|
1772
1978
|
}
|
|
1773
1979
|
const def = getAgentDefinition(agent);
|
|
1774
|
-
if (def.hookScriptPath && def.hookConfigPath)
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1980
|
+
if (def.hookScriptPath && def.hookConfigPath) {
|
|
1981
|
+
const enforcePrefix = opts.enforce && agent === Agent.Claude ? "AGS_HOOK_ENFORCE=1 " : "";
|
|
1982
|
+
await this.installStandardHookConfig({
|
|
1983
|
+
rootDir: opts.rootDir,
|
|
1984
|
+
agent,
|
|
1985
|
+
scriptRelPath: def.hookScriptPath,
|
|
1986
|
+
configRelPath: def.hookConfigPath,
|
|
1987
|
+
hookCmd: agent === Agent.Claude ? `${enforcePrefix}node "$CLAUDE_PROJECT_DIR/.claude/hooks/preedit-skill-loader.js"` : `node "${def.hookScriptPath}"`,
|
|
1988
|
+
report,
|
|
1989
|
+
promptPermission: opts.promptPermission
|
|
1990
|
+
});
|
|
1991
|
+
} else report.unsupported.push(agent);
|
|
1783
1992
|
}
|
|
1784
1993
|
return report;
|
|
1785
1994
|
}
|
|
@@ -1838,24 +2047,37 @@ var HookService = class {
|
|
|
1838
2047
|
file: opts.scriptRelPath,
|
|
1839
2048
|
action: "added"
|
|
1840
2049
|
});
|
|
1841
|
-
} else
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2050
|
+
} else {
|
|
2051
|
+
const installedVersion = (await fs_extra.default.readFile(scriptAbs, "utf8")).match(HOOK_VERSION_MARKER_RE)?.[1];
|
|
2052
|
+
if (installedVersion && installedVersion !== HOOK_SCRIPT_VERSION) {
|
|
2053
|
+
await fs_extra.default.writeFile(scriptAbs, UNIVERSAL_SKILL_LOADER_JS);
|
|
2054
|
+
opts.report.writes.push({
|
|
2055
|
+
agent: opts.agent,
|
|
2056
|
+
file: opts.scriptRelPath,
|
|
2057
|
+
action: "updated"
|
|
2058
|
+
});
|
|
2059
|
+
} else opts.report.writes.push({
|
|
2060
|
+
agent: opts.agent,
|
|
2061
|
+
file: opts.scriptRelPath,
|
|
2062
|
+
action: "skipped-existing"
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
1846
2065
|
const configAbs = path.default.join(opts.rootDir, opts.configRelPath);
|
|
1847
2066
|
const configExists = await fs_extra.default.pathExists(configAbs);
|
|
1848
2067
|
const existing = await this.readJson(configAbs);
|
|
1849
2068
|
let changed = false;
|
|
1850
2069
|
const permissions = this.ensureObject(existing, "permissions");
|
|
1851
2070
|
const allow = this.ensureArray(permissions, "allow");
|
|
1852
|
-
if (!allow.includes(
|
|
1853
|
-
|
|
1854
|
-
|
|
2071
|
+
if (!allow.includes("mcp__agent-skills-standard__*")) {
|
|
2072
|
+
if (opts.promptPermission ? await opts.promptPermission(opts.agent) : true) {
|
|
2073
|
+
permissions["allow"] = [CLAUDE_MCP_PERMISSION, ...allow];
|
|
2074
|
+
changed = true;
|
|
2075
|
+
}
|
|
1855
2076
|
}
|
|
1856
2077
|
const hooks = this.ensureObject(existing, "hooks");
|
|
1857
2078
|
const preToolUse = this.ensureArray(hooks, "PreToolUse");
|
|
1858
|
-
|
|
2079
|
+
const existingEntry = preToolUse.find((entry) => this.claudeHookEntryMatches(entry));
|
|
2080
|
+
if (!existingEntry) {
|
|
1859
2081
|
preToolUse.unshift({
|
|
1860
2082
|
matcher: "Edit|Write|MultiEdit|NotebookEdit",
|
|
1861
2083
|
hooks: [{
|
|
@@ -1865,6 +2087,12 @@ var HookService = class {
|
|
|
1865
2087
|
}]
|
|
1866
2088
|
});
|
|
1867
2089
|
changed = true;
|
|
2090
|
+
} else {
|
|
2091
|
+
const matchingHook = (existingEntry["hooks"] ?? []).find((h) => h["command"]?.includes("preedit-skill-loader"));
|
|
2092
|
+
if (matchingHook && matchingHook["command"] !== opts.hookCmd) {
|
|
2093
|
+
matchingHook["command"] = opts.hookCmd;
|
|
2094
|
+
changed = true;
|
|
2095
|
+
}
|
|
1868
2096
|
}
|
|
1869
2097
|
if (changed) {
|
|
1870
2098
|
await this.writeAtomic(configAbs, existing);
|
|
@@ -1887,7 +2115,7 @@ var HookService = class {
|
|
|
1887
2115
|
let changed = false;
|
|
1888
2116
|
const permissions = this.ensureObject(data, "permissions");
|
|
1889
2117
|
const allow = this.ensureArray(permissions, "allow");
|
|
1890
|
-
if (allow.includes(
|
|
2118
|
+
if (allow.includes("mcp__agent-skills-standard__*")) {
|
|
1891
2119
|
permissions["allow"] = allow.filter((p) => p !== CLAUDE_MCP_PERMISSION);
|
|
1892
2120
|
changed = true;
|
|
1893
2121
|
}
|
|
@@ -1992,7 +2220,7 @@ var HooksCommand = class {
|
|
|
1992
2220
|
this.configService = configService ?? new ConfigService();
|
|
1993
2221
|
this.hookService = hookService ?? new HookService();
|
|
1994
2222
|
}
|
|
1995
|
-
async run(action) {
|
|
2223
|
+
async run(action, options = {}) {
|
|
1996
2224
|
const config = await this.configService.loadConfig();
|
|
1997
2225
|
if (!config) {
|
|
1998
2226
|
console.log(picocolors.default.red("❌ .skillsrc not found. Run `agent-skills-standard init` first."));
|
|
@@ -2002,7 +2230,7 @@ var HooksCommand = class {
|
|
|
2002
2230
|
const agents = config.agents ?? [];
|
|
2003
2231
|
switch (action) {
|
|
2004
2232
|
case "status": return this.actionStatus(cwd, agents);
|
|
2005
|
-
case "install": return this.actionInstall(cwd, agents);
|
|
2233
|
+
case "install": return this.actionInstall(cwd, agents, options.enforce ?? false);
|
|
2006
2234
|
case "uninstall": return this.actionUninstall(cwd, agents);
|
|
2007
2235
|
default: console.log(picocolors.default.red(`Unknown action: ${action}`), "\nValid actions: status, install, uninstall");
|
|
2008
2236
|
}
|
|
@@ -2023,11 +2251,13 @@ var HooksCommand = class {
|
|
|
2023
2251
|
for (const f of row.files) console.log(` ${picocolors.default.gray(f)}`);
|
|
2024
2252
|
}
|
|
2025
2253
|
}
|
|
2026
|
-
async actionInstall(cwd, agents) {
|
|
2254
|
+
async actionInstall(cwd, agents, enforce) {
|
|
2027
2255
|
console.log(picocolors.default.cyan("\n🪝 Installing skill-loader hooks..."));
|
|
2256
|
+
if (enforce) console.log(picocolors.default.yellow(" --enforce: Claude will BLOCK edits to SOUL.md, MEMORY.md, .env*, .ssh/, and credentials*.json|yaml."));
|
|
2028
2257
|
const report = await this.hookService.install({
|
|
2029
2258
|
rootDir: cwd,
|
|
2030
|
-
agents
|
|
2259
|
+
agents,
|
|
2260
|
+
enforce
|
|
2031
2261
|
});
|
|
2032
2262
|
this.printReport(report);
|
|
2033
2263
|
}
|
|
@@ -2325,16 +2555,29 @@ var InitService = class {
|
|
|
2325
2555
|
#
|
|
2326
2556
|
`;
|
|
2327
2557
|
const configPath = path.default.join(cwd, ".skillsrc");
|
|
2328
|
-
await fs_extra.default.outputFile(configPath, commentHeader + js_yaml.
|
|
2558
|
+
await fs_extra.default.outputFile(configPath, commentHeader + js_yaml.dump(config));
|
|
2329
2559
|
}
|
|
2330
2560
|
};
|
|
2331
2561
|
//#endregion
|
|
2332
2562
|
//#region src/services/GithubService.ts
|
|
2563
|
+
/** A downloaded file failed its integrity or size check. */
|
|
2564
|
+
var IntegrityError = class extends Error {
|
|
2565
|
+
constructor(message) {
|
|
2566
|
+
super(message);
|
|
2567
|
+
this.name = "IntegrityError";
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
/** Computes the same blob SHA-1 git itself would (`git hash-object`), for comparison against a tree API `sha`. */
|
|
2571
|
+
function gitBlobSha1(content) {
|
|
2572
|
+
const header = Buffer.from(`blob ${content.byteLength}\0`, "utf8");
|
|
2573
|
+
return (0, node_crypto.createHash)("sha1").update(Buffer.concat([header, content])).digest("hex");
|
|
2574
|
+
}
|
|
2333
2575
|
/**
|
|
2334
2576
|
* Service for interacting with the GitHub API and fetching raw file content.
|
|
2335
2577
|
* Handles repository tree discovery, file downloads, and URL parsing.
|
|
2336
2578
|
*/
|
|
2337
2579
|
var GithubService = class {
|
|
2580
|
+
token;
|
|
2338
2581
|
baseUrl = "https://api.github.com";
|
|
2339
2582
|
rawBaseUrl = "https://raw.githubusercontent.com";
|
|
2340
2583
|
constructor(token) {
|
|
@@ -2367,25 +2610,35 @@ var GithubService = class {
|
|
|
2367
2610
|
}
|
|
2368
2611
|
}
|
|
2369
2612
|
/**
|
|
2370
|
-
* Fetches
|
|
2371
|
-
*
|
|
2372
|
-
* @param repo Repository name
|
|
2373
|
-
* @param ref Git reference
|
|
2374
|
-
* @param path Path to the file
|
|
2375
|
-
* @returns File content as string or null if not found
|
|
2613
|
+
* Fetches raw file bytes from GitHub and validates their Git blob identity.
|
|
2614
|
+
* Text callers should use getRawFile; package callers use this byte-preserving path.
|
|
2376
2615
|
*/
|
|
2377
|
-
async
|
|
2616
|
+
async getRawFileBytes(owner, repo, ref, path, options) {
|
|
2378
2617
|
const url = `${this.rawBaseUrl}/${owner}/${repo}/${ref}/${path}`;
|
|
2379
2618
|
try {
|
|
2380
2619
|
const res = await (0, cross_fetch.default)(url);
|
|
2381
2620
|
if (!res.ok) return null;
|
|
2382
|
-
|
|
2621
|
+
const content = Buffer.from(await res.arrayBuffer());
|
|
2622
|
+
const maxBytes = options?.maxBytes ?? 1048576;
|
|
2623
|
+
if (content.byteLength > maxBytes) throw new IntegrityError(`${path}: ${content.byteLength} bytes exceeds the ${maxBytes}-byte limit`);
|
|
2624
|
+
if (options?.expectedSha) {
|
|
2625
|
+
const actualSha = gitBlobSha1(content);
|
|
2626
|
+
if (actualSha !== options.expectedSha) throw new IntegrityError(`${path}: blob sha mismatch (expected ${options.expectedSha}, got ${actualSha}) — download may be corrupted or tampered with`);
|
|
2627
|
+
}
|
|
2628
|
+
return content;
|
|
2383
2629
|
} catch (error) {
|
|
2630
|
+
if (error instanceof IntegrityError) throw error;
|
|
2384
2631
|
console.error(picocolors.default.red(`Failed to fetch file ${path}: ${error}`));
|
|
2385
2632
|
return null;
|
|
2386
2633
|
}
|
|
2387
2634
|
}
|
|
2388
2635
|
/**
|
|
2636
|
+
* Fetches raw file content as UTF-8 for established text-only callers.
|
|
2637
|
+
*/
|
|
2638
|
+
async getRawFile(owner, repo, ref, path, options) {
|
|
2639
|
+
return (await this.getRawFileBytes(owner, repo, ref, path, options))?.toString("utf8") ?? null;
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2389
2642
|
* Retrieves the latest release tag name for a repository.
|
|
2390
2643
|
* @param owner Repository owner
|
|
2391
2644
|
* @param repo Repository name
|
|
@@ -2417,34 +2670,76 @@ var GithubService = class {
|
|
|
2417
2670
|
}
|
|
2418
2671
|
}
|
|
2419
2672
|
/**
|
|
2420
|
-
* Downloads multiple files concurrently with a limit.
|
|
2673
|
+
* Downloads multiple text files concurrently with a limit. Existing text
|
|
2674
|
+
* consumers retain UTF-8 strings; package sync uses the byte variant below.
|
|
2421
2675
|
*/
|
|
2422
2676
|
async downloadFilesConcurrent(tasks, concurrency = 10) {
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2677
|
+
return this.downloadFilesConcurrentWith(tasks, concurrency, (task) => this.getRawFile(task.owner, task.repo, task.ref, task.path, { expectedSha: task.sha }));
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Downloads multiple files without decoding their response bytes.
|
|
2681
|
+
*/
|
|
2682
|
+
async downloadFilesConcurrentBytes(tasks, concurrency = 10) {
|
|
2683
|
+
return this.downloadFilesConcurrentWith(tasks, concurrency, (task) => this.getRawFileBytes(task.owner, task.repo, task.ref, task.path, { expectedSha: task.sha }));
|
|
2684
|
+
}
|
|
2685
|
+
async downloadFilesConcurrentWith(tasks, concurrency, download) {
|
|
2686
|
+
const outcomes = Array(tasks.length);
|
|
2687
|
+
let nextIndex = 0;
|
|
2426
2688
|
const worker = async () => {
|
|
2427
|
-
while (
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
content
|
|
2434
|
-
|
|
2689
|
+
while (nextIndex < tasks.length) {
|
|
2690
|
+
const index = nextIndex++;
|
|
2691
|
+
const task = tasks[index];
|
|
2692
|
+
if (!task) continue;
|
|
2693
|
+
try {
|
|
2694
|
+
const content = await download(task);
|
|
2695
|
+
outcomes[index] = content === null ? {
|
|
2696
|
+
kind: "failed",
|
|
2697
|
+
path: task.path,
|
|
2698
|
+
reason: "not found"
|
|
2699
|
+
} : {
|
|
2700
|
+
kind: "ok",
|
|
2701
|
+
path: task.path,
|
|
2702
|
+
content
|
|
2703
|
+
};
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
outcomes[index] = {
|
|
2706
|
+
kind: "failed",
|
|
2707
|
+
path: task.path,
|
|
2708
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2435
2711
|
}
|
|
2436
2712
|
};
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2713
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, worker));
|
|
2714
|
+
const ok = [];
|
|
2715
|
+
const failed = [];
|
|
2716
|
+
for (const outcome of outcomes) {
|
|
2717
|
+
if (!outcome) continue;
|
|
2718
|
+
if (outcome.kind === "ok") ok.push({
|
|
2719
|
+
path: outcome.path,
|
|
2720
|
+
content: outcome.content
|
|
2721
|
+
});
|
|
2722
|
+
else failed.push({
|
|
2723
|
+
path: outcome.path,
|
|
2724
|
+
reason: outcome.reason
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
return {
|
|
2728
|
+
ok,
|
|
2729
|
+
failed
|
|
2730
|
+
};
|
|
2440
2731
|
}
|
|
2441
2732
|
/**
|
|
2442
|
-
* Parses a GitHub URL into owner and repository name.
|
|
2733
|
+
* Parses a GitHub URL into owner and repository name. Anchored to the
|
|
2734
|
+
* start of the string so a URL like `https://evil.com/?x=github.com/a/b`
|
|
2735
|
+
* — where "github.com/owner/repo" only appears as a substring, not as the
|
|
2736
|
+
* actual host — is correctly rejected instead of silently resolving to
|
|
2737
|
+
* `a/b` on the real github.com API.
|
|
2443
2738
|
* @param url The GitHub URL to parse
|
|
2444
2739
|
* @returns Object containing owner and repo, or null if invalid
|
|
2445
2740
|
*/
|
|
2446
2741
|
static parseGitHubUrl(url) {
|
|
2447
|
-
const m = url.match(
|
|
2742
|
+
const m = url.match(/^https?:\/\/(?:www\.)?github\.com\/([^/\s]+)\/([^/\s]+)/i);
|
|
2448
2743
|
if (!m) return null;
|
|
2449
2744
|
return {
|
|
2450
2745
|
owner: m[1],
|
|
@@ -2563,7 +2858,7 @@ var InitCommand = class {
|
|
|
2563
2858
|
const { frameworkChoices, agentChoices, defaultFramework } = this.initService.getPromptChoices(context, categories);
|
|
2564
2859
|
const answers = await inquirer.default.prompt([
|
|
2565
2860
|
{
|
|
2566
|
-
type: "
|
|
2861
|
+
type: "rawlist",
|
|
2567
2862
|
name: "framework",
|
|
2568
2863
|
message: "Select Framework:",
|
|
2569
2864
|
choices: frameworkChoices,
|
|
@@ -2589,7 +2884,7 @@ var InitCommand = class {
|
|
|
2589
2884
|
message: "Enable MCP server integration?",
|
|
2590
2885
|
default: true
|
|
2591
2886
|
}, {
|
|
2592
|
-
type: "
|
|
2887
|
+
type: "rawlist",
|
|
2593
2888
|
name: "mcpScope",
|
|
2594
2889
|
message: "Where should sync write MCP configs?",
|
|
2595
2890
|
when: (a) => a.mcpEnabled === true,
|
|
@@ -2717,7 +3012,7 @@ var ListSkillsCommand = class {
|
|
|
2717
3012
|
value: f.id
|
|
2718
3013
|
}));
|
|
2719
3014
|
framework = (await inquirer.default.prompt([{
|
|
2720
|
-
type: "
|
|
3015
|
+
type: "rawlist",
|
|
2721
3016
|
name: "framework",
|
|
2722
3017
|
message: "Select framework to list available skills for:",
|
|
2723
3018
|
choices,
|
|
@@ -2850,11 +3145,17 @@ var getTargets = (home = os.default.homedir()) => {
|
|
|
2850
3145
|
* a user-home file is about to be modified.
|
|
2851
3146
|
*/
|
|
2852
3147
|
var McpConfigService = class {
|
|
2853
|
-
/**
|
|
3148
|
+
/**
|
|
3149
|
+
* Returns the standard MCP server entry our CLI proposes. Defaults to the
|
|
3150
|
+
* version this CLI was released with rather than an unversioned `npx -y
|
|
3151
|
+
* <package>`, which would otherwise pull whatever is latest on npm the
|
|
3152
|
+
* moment the agent starts — an unpinned update-drift surface (AST07).
|
|
3153
|
+
* Pass an explicit `version` (from `mcp.version` in .skillsrc) to override.
|
|
3154
|
+
*/
|
|
2854
3155
|
buildEntry(version) {
|
|
2855
3156
|
return {
|
|
2856
3157
|
command: "npx",
|
|
2857
|
-
args: ["-y",
|
|
3158
|
+
args: ["-y", `${PACKAGE}@${version || "0.6.0"}`]
|
|
2858
3159
|
};
|
|
2859
3160
|
}
|
|
2860
3161
|
/** For testing: allows overriding the home directory. */
|
|
@@ -3402,10 +3703,7 @@ var AgentBridgeService = class {
|
|
|
3402
3703
|
case "cursor":
|
|
3403
3704
|
content += `---\ndescription: ${commonDescription}\nglobs: ['**/*']\nalwaysApply: true\n---\n\n`;
|
|
3404
3705
|
break;
|
|
3405
|
-
case "copilot":
|
|
3406
|
-
content += `---\ndescription: ${commonDescription}\napplyTo: '**/*'\n---\n\n`;
|
|
3407
|
-
break;
|
|
3408
|
-
case "none": break;
|
|
3706
|
+
case "copilot": content += `---\ndescription: ${commonDescription}\napplyTo: '**/*'\n---\n\n`;
|
|
3409
3707
|
}
|
|
3410
3708
|
if (agentId === Agent.Claude) {
|
|
3411
3709
|
const claudeProtocol = [
|
|
@@ -3543,6 +3841,7 @@ var IndexGeneratorService = class {};
|
|
|
3543
3841
|
* Centralized patterns for detecting and sanitizing prompt injection.
|
|
3544
3842
|
* Shared between the CLI and CI scanning scripts.
|
|
3545
3843
|
*/
|
|
3844
|
+
/** Applied to the SKILL.md frontmatter `description` field. Always error-level. */
|
|
3546
3845
|
var INJECTION_PATTERNS = [
|
|
3547
3846
|
/ignore\s+(?:all\s+|previous\s+|prior\s+)*(?:instructions?|rules?|guidance)/gi,
|
|
3548
3847
|
/you\s+(?:must|should|shall|will)\s+(?:now|immediately)\b/gi,
|
|
@@ -3550,6 +3849,23 @@ var INJECTION_PATTERNS = [
|
|
|
3550
3849
|
/(?:^|\n)\s*-{3,}\s*(?:\n|$)/g,
|
|
3551
3850
|
/<(?:script|iframe|style)[^>]*\s*>[\s\S]*?<\/(?:script|iframe|style)[^>]*\s*>/gi
|
|
3552
3851
|
];
|
|
3852
|
+
new RegExp(`[${[
|
|
3853
|
+
8203,
|
|
3854
|
+
8204,
|
|
3855
|
+
8205,
|
|
3856
|
+
8206,
|
|
3857
|
+
8207,
|
|
3858
|
+
8234,
|
|
3859
|
+
8235,
|
|
3860
|
+
8236,
|
|
3861
|
+
8237,
|
|
3862
|
+
8238,
|
|
3863
|
+
8294,
|
|
3864
|
+
8295,
|
|
3865
|
+
8296,
|
|
3866
|
+
8297,
|
|
3867
|
+
65279
|
|
3868
|
+
].map((cp) => String.fromCodePoint(cp)).join("")}]`, "g");
|
|
3553
3869
|
//#endregion
|
|
3554
3870
|
//#region src/services/MetadataReader.ts
|
|
3555
3871
|
/**
|
|
@@ -3629,7 +3945,7 @@ var MetadataReader = class {
|
|
|
3629
3945
|
try {
|
|
3630
3946
|
const frontmatterMatch = (await fs_extra.default.readFile(skillPath, "utf8")).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
3631
3947
|
if (!frontmatterMatch) return null;
|
|
3632
|
-
const fm = js_yaml.
|
|
3948
|
+
const fm = js_yaml.load(frontmatterMatch[1]);
|
|
3633
3949
|
const priorityMatch = frontmatterMatch[2].match(/## \*\*Priority:\s*([^*]+)\*\*/);
|
|
3634
3950
|
const priority = priorityMatch ? priorityMatch[1].trim() : "P1";
|
|
3635
3951
|
const rawTriggers = (fm?.metadata || {})?.triggers || {};
|
|
@@ -3858,6 +4174,14 @@ var IndexGeneratorServiceImpl = class extends IndexGeneratorService {
|
|
|
3858
4174
|
coveredCategories.add("quality-engineering");
|
|
3859
4175
|
routerRows.push("| QE workflow | `<SKILLS>/quality-engineering/_INDEX.md` |");
|
|
3860
4176
|
}
|
|
4177
|
+
if (availableCategories.includes("system-design")) {
|
|
4178
|
+
coveredCategories.add("system-design");
|
|
4179
|
+
routerRows.push("| Architecture or scale request | `<SKILLS>/system-design/_INDEX.md` |");
|
|
4180
|
+
}
|
|
4181
|
+
if (availableCategories.includes("cybersecurity")) {
|
|
4182
|
+
coveredCategories.add("cybersecurity");
|
|
4183
|
+
routerRows.push("| Cybersecurity investigation or authorized exercise | `<SKILLS>/cybersecurity/_INDEX.md` |");
|
|
4184
|
+
}
|
|
3861
4185
|
return [
|
|
3862
4186
|
"## Agent Skills Index",
|
|
3863
4187
|
"",
|
|
@@ -3915,9 +4239,24 @@ var IndexGeneratorServiceImpl = class extends IndexGeneratorService {
|
|
|
3915
4239
|
//#endregion
|
|
3916
4240
|
//#region src/services/SkillSyncService.ts
|
|
3917
4241
|
/**
|
|
4242
|
+
* A selected registry category or package could not be assembled completely.
|
|
4243
|
+
* Callers must not write installations or lockfiles from this partial result.
|
|
4244
|
+
*/
|
|
4245
|
+
var SkillAssemblyError = class extends Error {
|
|
4246
|
+
failures;
|
|
4247
|
+
constructor(failures) {
|
|
4248
|
+
super(`Failed to assemble selected skills: ${failures.join(", ")}`);
|
|
4249
|
+
this.failures = failures;
|
|
4250
|
+
this.name = "SkillAssemblyError";
|
|
4251
|
+
}
|
|
4252
|
+
};
|
|
4253
|
+
/**
|
|
3918
4254
|
* Service responsible for synchronizing agent skills from a remote registry.
|
|
3919
4255
|
*/
|
|
3920
4256
|
var SkillSyncService = class {
|
|
4257
|
+
githubService;
|
|
4258
|
+
failedPackagesByCategory = /* @__PURE__ */ new Map();
|
|
4259
|
+
failedCategories = /* @__PURE__ */ new Set();
|
|
3921
4260
|
constructor(githubService) {
|
|
3922
4261
|
this.githubService = githubService;
|
|
3923
4262
|
}
|
|
@@ -3925,11 +4264,13 @@ var SkillSyncService = class {
|
|
|
3925
4264
|
* Assembles skills from the remote registry based on provided categories and configuration.
|
|
3926
4265
|
*/
|
|
3927
4266
|
async assembleSkills(categories, config) {
|
|
4267
|
+
this.failedPackagesByCategory.clear();
|
|
4268
|
+
this.failedCategories.clear();
|
|
3928
4269
|
const collected = [];
|
|
3929
4270
|
const githubMatch = GithubService.parseGitHubUrl(config.registry);
|
|
3930
4271
|
if (!githubMatch) {
|
|
3931
4272
|
console.log(picocolors.default.red("Error: Only GitHub registries supported."));
|
|
3932
|
-
|
|
4273
|
+
throw new SkillAssemblyError(["registry is not a GitHub repository"]);
|
|
3933
4274
|
}
|
|
3934
4275
|
const { owner, repo } = githubMatch;
|
|
3935
4276
|
for (const category of categories) {
|
|
@@ -3938,15 +4279,23 @@ var SkillSyncService = class {
|
|
|
3938
4279
|
console.log(picocolors.default.gray(` - Discovering ${category} (${ref})...`));
|
|
3939
4280
|
const treeData = await this.githubService.getRepoTree(owner, repo, ref);
|
|
3940
4281
|
if (!treeData) {
|
|
4282
|
+
this.failedCategories.add(category);
|
|
3941
4283
|
console.log(picocolors.default.red(` ❌ Failed to fetch ${category}@${ref}.`));
|
|
3942
4284
|
continue;
|
|
3943
4285
|
}
|
|
3944
4286
|
const foldersToSync = this.identifyFoldersToSync(category, catConfig, treeData.tree);
|
|
3945
4287
|
for (const absOrRelSkill of foldersToSync) {
|
|
3946
4288
|
const skill = await this.fetchSkill(owner, repo, ref, category, absOrRelSkill, treeData.tree);
|
|
3947
|
-
if (skill)
|
|
4289
|
+
if (skill) {
|
|
4290
|
+
collected.push(skill);
|
|
4291
|
+
continue;
|
|
4292
|
+
}
|
|
4293
|
+
const [sourceCategory, skillName] = absOrRelSkill.includes("/") ? absOrRelSkill.split("/") : [category, absOrRelSkill];
|
|
4294
|
+
if (sourceCategory && skillName) this.markPackageFailed(sourceCategory, skillName);
|
|
3948
4295
|
}
|
|
3949
4296
|
}
|
|
4297
|
+
const failures = [...this.failedCategories, ...Array.from(this.failedPackagesByCategory).flatMap(([category, skills]) => Array.from(skills, (skill) => `${category}/${skill}`))];
|
|
4298
|
+
if (failures.length > 0) throw new SkillAssemblyError(failures);
|
|
3950
4299
|
return collected;
|
|
3951
4300
|
}
|
|
3952
4301
|
/**
|
|
@@ -3955,75 +4304,89 @@ var SkillSyncService = class {
|
|
|
3955
4304
|
async writeSkills(skills, config, agents) {
|
|
3956
4305
|
const overrides = config.custom_overrides || [];
|
|
3957
4306
|
const fetchedSkillsByCategory = {};
|
|
3958
|
-
for (const skill of skills)
|
|
3959
|
-
if (!fetchedSkillsByCategory[skill.category]) fetchedSkillsByCategory[skill.category] = /* @__PURE__ */ new Set();
|
|
3960
|
-
fetchedSkillsByCategory[skill.category].add(skill.skill);
|
|
3961
|
-
}
|
|
4307
|
+
for (const skill of skills) (fetchedSkillsByCategory[skill.category] ??= /* @__PURE__ */ new Set()).add(skill.skill);
|
|
3962
4308
|
for (const agentId of agents) {
|
|
3963
|
-
const agentDef = SUPPORTED_AGENTS.find((
|
|
3964
|
-
if (!agentDef
|
|
4309
|
+
const agentDef = SUPPORTED_AGENTS.find((agent) => agent.id === agentId);
|
|
4310
|
+
if (!agentDef?.path) continue;
|
|
3965
4311
|
const basePath = agentDef.path;
|
|
3966
4312
|
await fs_extra.default.ensureDir(basePath);
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
if (await fs_extra.default.pathExists(categoryPath)) {
|
|
3970
|
-
const existingDirs = await fs_extra.default.readdir(categoryPath);
|
|
3971
|
-
for (const dir of existingDirs) if (!fetchedSkillsByCategory[category].has(dir)) {
|
|
3972
|
-
const fullPath = path.default.join(categoryPath, dir);
|
|
3973
|
-
if (!this.isOverridden(fullPath, overrides)) await fs_extra.default.remove(fullPath);
|
|
3974
|
-
}
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
for (const skill of skills) await this.writeSkillForAgent(agentId, skill, overrides, basePath);
|
|
4313
|
+
for (const skill of skills) if (!await this.writeSkillForAgent(agentId, skill, overrides, basePath)) this.markPackageFailed(skill.category, skill.skill);
|
|
4314
|
+
if (config.prune !== false) await this.pruneOrphanedSkills(basePath, fetchedSkillsByCategory, overrides);
|
|
3978
4315
|
console.log(picocolors.default.gray(` - Updated ${basePath}/ (${agentDef.name})`));
|
|
3979
4316
|
}
|
|
3980
4317
|
}
|
|
3981
4318
|
async writeSkillForAgent(agentId, skill, overrides, basePath) {
|
|
3982
4319
|
const isKiro = agentId === Agent.Kiro;
|
|
3983
|
-
const
|
|
3984
|
-
|
|
3985
|
-
|
|
4320
|
+
const skillPath = isKiro ? path.default.join(basePath, `${skill.category}-${skill.skill}`) : path.default.join(basePath, skill.category, skill.skill);
|
|
4321
|
+
if (!this.isPathSafe(skillPath, basePath)) {
|
|
4322
|
+
console.log(picocolors.default.red(` ❌ Security Error: Invalid skill path ${skill.category}/${skill.skill}`));
|
|
4323
|
+
throw new Error(`Invalid skill path ${skill.category}/${skill.skill}`);
|
|
4324
|
+
}
|
|
3986
4325
|
for (const fileItem of skill.files) {
|
|
3987
4326
|
const targetFilePath = path.default.join(skillPath, fileItem.name);
|
|
3988
|
-
if (this.isOverridden(targetFilePath, overrides)) {
|
|
3989
|
-
console.log(picocolors.default.yellow(` ⚠️ Skipping overridden: ${this.normalizePath(targetFilePath)}`));
|
|
3990
|
-
continue;
|
|
3991
|
-
}
|
|
3992
4327
|
if (!this.isPathSafe(targetFilePath, skillPath)) {
|
|
3993
4328
|
console.log(picocolors.default.red(` ❌ Security Error: Invalid path ${fileItem.name}`));
|
|
3994
|
-
|
|
4329
|
+
throw new Error(`Invalid path ${fileItem.name}`);
|
|
3995
4330
|
}
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
4331
|
+
}
|
|
4332
|
+
const stagingPath = path.default.join(path.default.dirname(skillPath), `.${path.default.basename(skillPath)}.ags-staging`);
|
|
4333
|
+
if (await fs_extra.default.pathExists(stagingPath)) throw new Error(`Refusing to replace ${skill.category}/${skill.skill}: staging path already exists at ${stagingPath}`);
|
|
4334
|
+
await fs_extra.default.ensureDir(path.default.dirname(skillPath));
|
|
4335
|
+
try {
|
|
4336
|
+
if (this.hasOverridesInsideSkill(skillPath, overrides)) await this.copyOverrides(skillPath, stagingPath, overrides);
|
|
4337
|
+
for (const fileItem of skill.files) {
|
|
4338
|
+
const targetFilePath = path.default.join(skillPath, fileItem.name);
|
|
4339
|
+
if (this.isOverridden(targetFilePath, overrides)) {
|
|
4340
|
+
console.log(picocolors.default.yellow(` ⚠️ Skipping overridden: ${this.normalizePath(targetFilePath)}`));
|
|
4341
|
+
continue;
|
|
4342
|
+
}
|
|
4343
|
+
const stagedFilePath = path.default.join(stagingPath, fileItem.name);
|
|
4344
|
+
let content = fileItem.bytes ?? fileItem.content;
|
|
4345
|
+
if (isKiro && fileItem.name === "SKILL.md") content = this.transformSkillForKiro(fileItem.content, skill.category);
|
|
4346
|
+
await fs_extra.default.outputFile(stagedFilePath, content);
|
|
4347
|
+
}
|
|
4348
|
+
await this.replaceSkillDirectory(stagingPath, skillPath);
|
|
4349
|
+
return true;
|
|
4350
|
+
} catch (error) {
|
|
4351
|
+
await fs_extra.default.remove(stagingPath);
|
|
4352
|
+
throw error;
|
|
3999
4353
|
}
|
|
4000
4354
|
}
|
|
4001
4355
|
async fetchSkill(owner, repo, ref, category, absOrRelSkill, tree) {
|
|
4002
4356
|
const [sourceCat, skillName] = absOrRelSkill.includes("/") ? absOrRelSkill.split("/") : [category, absOrRelSkill];
|
|
4357
|
+
if (!sourceCat || !skillName) return null;
|
|
4003
4358
|
const prefix = `skills/${sourceCat}/${skillName}/`;
|
|
4004
|
-
const
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
}).map((f) => ({
|
|
4359
|
+
const packageFiles = tree.filter((file) => file.type === "blob" && file.path.startsWith(prefix) && this.isPackageResource(file.path.slice(prefix.length)));
|
|
4360
|
+
const rootAttributionFiles = tree.filter((file) => file.type === "blob" && this.isRootAttributionFile(file.path) && !packageFiles.some((packageFile) => packageFile.path.slice(prefix.length).toLowerCase() === file.path.toLowerCase()));
|
|
4361
|
+
const downloadTasks = [...packageFiles, ...rootAttributionFiles].sort((left, right) => left.path.localeCompare(right.path)).map((file) => ({
|
|
4008
4362
|
owner,
|
|
4009
4363
|
repo,
|
|
4010
4364
|
ref,
|
|
4011
|
-
path:
|
|
4365
|
+
path: file.path,
|
|
4366
|
+
sha: file.sha
|
|
4012
4367
|
}));
|
|
4013
|
-
const files = await this.githubService.
|
|
4014
|
-
|
|
4368
|
+
const { ok: files, failed } = await this.githubService.downloadFilesConcurrentBytes(downloadTasks);
|
|
4369
|
+
for (const failure of failed) console.log(picocolors.default.red(` ❌ ${sourceCat}/${skillName}: ${failure.path} — ${failure.reason}`));
|
|
4370
|
+
if (!packageFiles.some((file) => file.path === `${prefix}SKILL.md`) || failed.length > 0 || files.length !== downloadTasks.length) return null;
|
|
4015
4371
|
console.log(picocolors.default.gray(` + Fetched ${sourceCat}/${skillName} (${files.length} files)`));
|
|
4016
4372
|
return {
|
|
4017
4373
|
category: sourceCat,
|
|
4018
4374
|
skill: skillName,
|
|
4019
|
-
files: files.map((
|
|
4020
|
-
name:
|
|
4021
|
-
content:
|
|
4375
|
+
files: files.map((file) => ({
|
|
4376
|
+
name: file.path.startsWith(prefix) ? file.path.slice(prefix.length) : file.path,
|
|
4377
|
+
content: file.content.toString("utf8"),
|
|
4378
|
+
bytes: file.content
|
|
4022
4379
|
}))
|
|
4023
4380
|
};
|
|
4024
4381
|
}
|
|
4025
4382
|
identifyFoldersToSync(category, catConfig, tree) {
|
|
4026
|
-
const
|
|
4383
|
+
const hasSkillMd = tree.some((f) => f.type === "blob" && f.path.startsWith(`skills/${category}/`) && f.path.endsWith("/SKILL.md"));
|
|
4384
|
+
const skillFolders = new Set(tree.filter((f) => {
|
|
4385
|
+
if (!f.path.startsWith(`skills/${category}/`)) return false;
|
|
4386
|
+
if (hasSkillMd) return f.type === "blob" && f.path.endsWith("/SKILL.md");
|
|
4387
|
+
const rel = f.path.slice(`skills/${category}/`.length);
|
|
4388
|
+
return !rel.startsWith("references/") && rel !== "references" && rel !== "_INDEX.md";
|
|
4389
|
+
}).map((f) => f.path.split("/")[2]).filter(Boolean));
|
|
4027
4390
|
const folders = Array.from(skillFolders).filter((folder) => {
|
|
4028
4391
|
if (catConfig.include && !catConfig.include.includes(folder)) return false;
|
|
4029
4392
|
if (catConfig.exclude && catConfig.exclude.includes(folder)) return false;
|
|
@@ -4035,21 +4398,128 @@ var SkillSyncService = class {
|
|
|
4035
4398
|
expandAbsoluteInclude(absSkill, folders, tree) {
|
|
4036
4399
|
const [targetCat, targetSkill] = absSkill.split("/");
|
|
4037
4400
|
if (!targetCat || !targetSkill) return;
|
|
4038
|
-
if (targetSkill === "*")
|
|
4039
|
-
const
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4401
|
+
if (targetSkill === "*") {
|
|
4402
|
+
const hasTargetSkillMd = tree.some((f) => f.type === "blob" && f.path.startsWith(`skills/${targetCat}/`) && f.path.endsWith("/SKILL.md"));
|
|
4403
|
+
new Set(tree.filter((f) => {
|
|
4404
|
+
if (!f.path.startsWith(`skills/${targetCat}/`)) return false;
|
|
4405
|
+
if (hasTargetSkillMd) return f.type === "blob" && f.path.endsWith("/SKILL.md");
|
|
4406
|
+
const rel = f.path.slice(`skills/${targetCat}/`.length);
|
|
4407
|
+
return !rel.startsWith("references/") && rel !== "references" && rel !== "_INDEX.md";
|
|
4408
|
+
}).map((f) => f.path.split("/")[2]).filter(Boolean)).forEach((s) => {
|
|
4409
|
+
const fullPath = `${targetCat}/${s}`;
|
|
4410
|
+
if (!folders.includes(fullPath)) folders.push(fullPath);
|
|
4411
|
+
});
|
|
4412
|
+
} else if (!folders.includes(absSkill)) {
|
|
4413
|
+
if (tree.some((f) => f.path.startsWith(`skills/${targetCat}/${targetSkill}/`))) folders.push(absSkill);
|
|
4414
|
+
else console.log(picocolors.default.yellow(` ⚠️ Absolute include ${absSkill} not found in repository.`));
|
|
4415
|
+
}
|
|
4044
4416
|
}
|
|
4417
|
+
/**
|
|
4418
|
+
* Kiro has no file/keyword routing concept, so `metadata.triggers` is
|
|
4419
|
+
* intentionally dropped here (Kiro users get skills with no auto-routing
|
|
4420
|
+
* — a known, documented limitation, not a bug). Every other declared
|
|
4421
|
+
* field — including the optional Universal-Skill-Format fields
|
|
4422
|
+
* (version/risk_tier/allowed-tools/permissions/content_hash/signature) —
|
|
4423
|
+
* is preserved rather than silently discarded.
|
|
4424
|
+
*
|
|
4425
|
+
* Uses a real YAML parse + js-yaml dump (matching SpecialistTransformer)
|
|
4426
|
+
* instead of regex-extract-and-reinterpolate: the previous implementation
|
|
4427
|
+
* built `description: ${description}` via raw string interpolation, which
|
|
4428
|
+
* had the same YAML-key-injection exposure fixed elsewhere in this PR — a
|
|
4429
|
+
* description containing a quote+newline could inject a new top-level key
|
|
4430
|
+
* into the emitted Kiro frontmatter.
|
|
4431
|
+
*/
|
|
4045
4432
|
transformSkillForKiro(content, category) {
|
|
4046
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n
|
|
4433
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
4047
4434
|
if (!frontmatterMatch) return content;
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4435
|
+
let metadata;
|
|
4436
|
+
try {
|
|
4437
|
+
metadata = js_yaml.load(frontmatterMatch[1]) ?? {};
|
|
4438
|
+
} catch {
|
|
4439
|
+
return content;
|
|
4440
|
+
}
|
|
4441
|
+
const body = frontmatterMatch[2];
|
|
4442
|
+
const name = typeof metadata.name === "string" ? metadata.name : "";
|
|
4443
|
+
const displayName = `${category.charAt(0).toUpperCase() + category.slice(1)} - ${name}`;
|
|
4444
|
+
const fm = {
|
|
4445
|
+
...metadata,
|
|
4446
|
+
name: displayName,
|
|
4447
|
+
description: typeof metadata.description === "string" ? metadata.description : ""
|
|
4448
|
+
};
|
|
4449
|
+
const kiroMetadata = fm.metadata;
|
|
4450
|
+
if (this.isRecord(kiroMetadata)) {
|
|
4451
|
+
const preservedMetadata = { ...kiroMetadata };
|
|
4452
|
+
delete preservedMetadata.triggers;
|
|
4453
|
+
fm.metadata = preservedMetadata;
|
|
4454
|
+
}
|
|
4455
|
+
return `---\n${js_yaml.dump(fm, { lineWidth: -1 }).trimEnd()}\n---\n\n${body}`;
|
|
4456
|
+
}
|
|
4457
|
+
isPackageResource(relativePath) {
|
|
4458
|
+
return relativePath === "SKILL.md" || /^(references|scripts|assets)\//.test(relativePath) || this.isRootAttributionFile(relativePath);
|
|
4459
|
+
}
|
|
4460
|
+
isRootAttributionFile(filePath) {
|
|
4461
|
+
return !filePath.includes("/") && /^(LICENSE|NOTICE)(?:\.(?:md|txt))?$/i.test(filePath);
|
|
4462
|
+
}
|
|
4463
|
+
markPackageFailed(category, skill) {
|
|
4464
|
+
let failedSkills = this.failedPackagesByCategory.get(category);
|
|
4465
|
+
if (!failedSkills) {
|
|
4466
|
+
failedSkills = /* @__PURE__ */ new Set();
|
|
4467
|
+
this.failedPackagesByCategory.set(category, failedSkills);
|
|
4468
|
+
}
|
|
4469
|
+
failedSkills.add(skill);
|
|
4470
|
+
}
|
|
4471
|
+
async pruneOrphanedSkills(basePath, fetchedSkillsByCategory, overrides) {
|
|
4472
|
+
for (const [category, fetchedSkills] of Object.entries(fetchedSkillsByCategory)) {
|
|
4473
|
+
if (this.failedCategories.has(category)) continue;
|
|
4474
|
+
const categoryPath = path.default.join(basePath, category);
|
|
4475
|
+
if (!await fs_extra.default.pathExists(categoryPath)) continue;
|
|
4476
|
+
const failedSkills = this.failedPackagesByCategory.get(category);
|
|
4477
|
+
const existingDirs = await fs_extra.default.readdir(categoryPath);
|
|
4478
|
+
for (const directory of existingDirs) {
|
|
4479
|
+
const fullPath = path.default.join(categoryPath, directory);
|
|
4480
|
+
if (fetchedSkills.has(directory) || failedSkills?.has(directory) || this.isOverridden(fullPath, overrides)) continue;
|
|
4481
|
+
await fs_extra.default.remove(fullPath);
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
hasOverridesInsideSkill(skillPath, overrides) {
|
|
4486
|
+
const skillRelativePath = this.normalizePath(skillPath);
|
|
4487
|
+
const skillSuffix = skillRelativePath.split("/").slice(-2).join("/");
|
|
4488
|
+
return overrides.some((override) => {
|
|
4489
|
+
const normalizedOverride = override.replace(/\\/g, "/").replace(/\/$/, "");
|
|
4490
|
+
return this.isOverridden(skillPath, [override]) || normalizedOverride.startsWith(`${skillRelativePath}/`) || normalizedOverride.includes(`/${skillRelativePath}/`) || normalizedOverride === skillSuffix || normalizedOverride.startsWith(`${skillSuffix}/`) || normalizedOverride.includes(`/${skillSuffix}/`);
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
async copyOverrides(sourcePath, targetPath, overrides) {
|
|
4494
|
+
if (!await fs_extra.default.pathExists(sourcePath)) return;
|
|
4495
|
+
const sourceStat = await fs_extra.default.lstat(sourcePath);
|
|
4496
|
+
if (sourceStat.isSymbolicLink() || !sourceStat.isDirectory()) {
|
|
4497
|
+
console.log(picocolors.default.yellow(` ⚠️ Skipping override copy outside installation root: ${this.normalizePath(sourcePath)}`));
|
|
4498
|
+
return;
|
|
4499
|
+
}
|
|
4500
|
+
const entries = await fs_extra.default.readdir(sourcePath, { withFileTypes: true });
|
|
4501
|
+
for (const entry of entries) {
|
|
4502
|
+
const source = path.default.join(sourcePath, entry.name);
|
|
4503
|
+
const target = path.default.join(targetPath, entry.name);
|
|
4504
|
+
if (entry.isDirectory()) await this.copyOverrides(source, target, overrides);
|
|
4505
|
+
else if (entry.isFile() && this.isOverridden(source, overrides)) await fs_extra.default.copy(source, target);
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
async replaceSkillDirectory(stagingPath, skillPath) {
|
|
4509
|
+
const backupPath = `${skillPath}.ags-backup`;
|
|
4510
|
+
if (await fs_extra.default.pathExists(backupPath)) throw new Error(`Refusing to replace ${this.normalizePath(skillPath)}: backup path already exists at ${backupPath}`);
|
|
4511
|
+
const hadExistingSkill = await fs_extra.default.pathExists(skillPath);
|
|
4512
|
+
if (hadExistingSkill) await fs_extra.default.move(skillPath, backupPath, { overwrite: false });
|
|
4513
|
+
try {
|
|
4514
|
+
await fs_extra.default.move(stagingPath, skillPath, { overwrite: false });
|
|
4515
|
+
} catch (error) {
|
|
4516
|
+
if (hadExistingSkill) await fs_extra.default.move(backupPath, skillPath, { overwrite: false });
|
|
4517
|
+
throw error;
|
|
4518
|
+
}
|
|
4519
|
+
if (hadExistingSkill) await fs_extra.default.remove(backupPath);
|
|
4520
|
+
}
|
|
4521
|
+
isRecord(value) {
|
|
4522
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4053
4523
|
}
|
|
4054
4524
|
isOverridden(targetPath, overrides) {
|
|
4055
4525
|
const rel = this.normalizePath(targetPath);
|
|
@@ -4067,6 +4537,21 @@ var SkillSyncService = class {
|
|
|
4067
4537
|
}
|
|
4068
4538
|
};
|
|
4069
4539
|
//#endregion
|
|
4540
|
+
//#region src/services/utils/tomlEscape.ts
|
|
4541
|
+
/**
|
|
4542
|
+
* Escapes backslashes and double quotes for embedding inside any TOML basic
|
|
4543
|
+
* string — single-line `"..."` or multi-line `"""..."""`.
|
|
4544
|
+
*
|
|
4545
|
+
* Escaping every quote (not just runs of 3+) sidesteps the ambiguity around
|
|
4546
|
+
* multi-line basic strings, where an unescaped run of 3+ quote characters
|
|
4547
|
+
* can be misread as the closing delimiter: with every quote escaped, no
|
|
4548
|
+
* unescaped `"""` sequence can ever occur in the output, regardless of how
|
|
4549
|
+
* many consecutive quote characters the source content contains.
|
|
4550
|
+
*/
|
|
4551
|
+
function escapeTomlString(value) {
|
|
4552
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
4553
|
+
}
|
|
4554
|
+
//#endregion
|
|
4070
4555
|
//#region src/services/utils/WorkflowTransformer.ts
|
|
4071
4556
|
var WORKFLOW_ARGUMENTS = "Optional args: slug=<feature>, ticket=<id/url>, mode=interactive|autonomous|channel, channel=<id>, auto_continue=true|false, profile=business|hybrid|technical.";
|
|
4072
4557
|
/**
|
|
@@ -4133,8 +4618,10 @@ var WorkflowTransformer = class {
|
|
|
4133
4618
|
body: content
|
|
4134
4619
|
};
|
|
4135
4620
|
const descMatch = match[1].match(/description:\s*(.+)/);
|
|
4621
|
+
const rawDescription = descMatch ? descMatch[1].trim() : "";
|
|
4622
|
+
const quotedMatch = rawDescription.match(/^"(.*)"$|^'(.*)'$/);
|
|
4136
4623
|
return {
|
|
4137
|
-
description:
|
|
4624
|
+
description: quotedMatch ? quotedMatch[1] ?? quotedMatch[2] : rawDescription,
|
|
4138
4625
|
body: match[2]
|
|
4139
4626
|
};
|
|
4140
4627
|
}
|
|
@@ -4167,13 +4654,13 @@ ${body}`;
|
|
|
4167
4654
|
* User invokes via /<name> [arguments]
|
|
4168
4655
|
*/
|
|
4169
4656
|
static toGeminiCommand(description, body) {
|
|
4170
|
-
return `description = "${description
|
|
4657
|
+
return `description = "${escapeTomlString(description)}"
|
|
4171
4658
|
prompt = """
|
|
4172
4659
|
Execute this workflow for: {{args}}
|
|
4173
4660
|
|
|
4174
4661
|
${WORKFLOW_ARGUMENTS}
|
|
4175
4662
|
|
|
4176
|
-
${body
|
|
4663
|
+
${escapeTomlString(body)}
|
|
4177
4664
|
"""
|
|
4178
4665
|
`;
|
|
4179
4666
|
}
|
|
@@ -4183,7 +4670,7 @@ ${body.replace(/"""/g, "\\\"\\\"\\\"")}
|
|
|
4183
4670
|
* User invokes via /prompt-name in Copilot chat.
|
|
4184
4671
|
*/
|
|
4185
4672
|
static toCopilotPrompt(description, body) {
|
|
4186
|
-
return `---\ndescription: "${description}"\n---\n${body}`;
|
|
4673
|
+
return `---\ndescription: "${description.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"\n---\n${body}`;
|
|
4187
4674
|
}
|
|
4188
4675
|
/**
|
|
4189
4676
|
* Agent Skill (SKILL.md) format.
|
|
@@ -4225,6 +4712,7 @@ ${body}
|
|
|
4225
4712
|
* Service responsible for synchronizing agent workflows from a remote registry.
|
|
4226
4713
|
*/
|
|
4227
4714
|
var WorkflowSyncService = class {
|
|
4715
|
+
githubService;
|
|
4228
4716
|
constructor(githubService) {
|
|
4229
4717
|
this.githubService = githubService;
|
|
4230
4718
|
}
|
|
@@ -4283,12 +4771,14 @@ var WorkflowSyncService = class {
|
|
|
4283
4771
|
if (Array.isArray(config.workflows)) return config.workflows.includes(this.workflowNameFromPath(f.path));
|
|
4284
4772
|
return false;
|
|
4285
4773
|
});
|
|
4286
|
-
const files = await this.githubService.downloadFilesConcurrent(workflowFiles.map((f) => ({
|
|
4774
|
+
const { ok: files, failed } = await this.githubService.downloadFilesConcurrent(workflowFiles.map((f) => ({
|
|
4287
4775
|
owner,
|
|
4288
4776
|
repo,
|
|
4289
4777
|
ref,
|
|
4290
|
-
path: f.path
|
|
4778
|
+
path: f.path,
|
|
4779
|
+
sha: f.sha
|
|
4291
4780
|
})));
|
|
4781
|
+
for (const failure of failed) console.log(picocolors.default.red(` ❌ ${failure.path} — ${failure.reason}`));
|
|
4292
4782
|
if (files.length > 0) {
|
|
4293
4783
|
console.log(picocolors.default.gray(` + Fetched ${files.length} workflows`));
|
|
4294
4784
|
return [{
|
|
@@ -4371,6 +4861,46 @@ var WorkflowSyncService = class {
|
|
|
4371
4861
|
};
|
|
4372
4862
|
//#endregion
|
|
4373
4863
|
//#region src/services/utils/SpecialistTransformer.ts
|
|
4864
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
4865
|
+
/**
|
|
4866
|
+
* Serializes frontmatter fields via js-yaml so every value — including a
|
|
4867
|
+
* `description` sourced from skill content — is safely quoted/escaped.
|
|
4868
|
+
* Interpolating raw strings into a hand-built YAML template let a crafted
|
|
4869
|
+
* description inject its own top-level keys (e.g. a second `tools:`) into
|
|
4870
|
+
* the emitted agent file; js-yaml can't be tricked that way.
|
|
4871
|
+
*/
|
|
4872
|
+
function dumpFrontmatter(fields) {
|
|
4873
|
+
return js_yaml.dump(fields, { lineWidth: -1 }).trimEnd();
|
|
4874
|
+
}
|
|
4875
|
+
/**
|
|
4876
|
+
* Codex is the only target with a native least-privilege primitive
|
|
4877
|
+
* (sandbox_mode). L2/L3 map to workspace-write; danger-full-access is never
|
|
4878
|
+
* auto-granted regardless of declared risk_tier — that always requires
|
|
4879
|
+
* explicit user configuration outside the sync pipeline.
|
|
4880
|
+
*/
|
|
4881
|
+
function sandboxModeFor(riskTier) {
|
|
4882
|
+
return riskTier === "L2" || riskTier === "L3" ? "workspace-write" : "read-only";
|
|
4883
|
+
}
|
|
4884
|
+
/** Summarizes whichever of risk_tier/allowed-tools/permissions a specialist declared, for a warning comment. */
|
|
4885
|
+
function summarizeDeclaredPermissions(metadata) {
|
|
4886
|
+
const parts = [];
|
|
4887
|
+
if (metadata.risk_tier) parts.push(`risk_tier: ${metadata.risk_tier}`);
|
|
4888
|
+
if (metadata["allowed-tools"]) parts.push(`allowed-tools: ${JSON.stringify(metadata["allowed-tools"])}`);
|
|
4889
|
+
if (metadata.permissions) parts.push(`permissions: ${JSON.stringify(metadata.permissions)}`);
|
|
4890
|
+
return parts.length > 0 ? parts.join("; ") : null;
|
|
4891
|
+
}
|
|
4892
|
+
/**
|
|
4893
|
+
* For platforms with no way to express a declared risk_tier/permissions/
|
|
4894
|
+
* allowed-tools, prepend a visible warning rather than silently dropping
|
|
4895
|
+
* the declaration — a persona authored as restricted for one platform
|
|
4896
|
+
* should not read as unconstrained everywhere else without at least a
|
|
4897
|
+
* trace of what was intended.
|
|
4898
|
+
*/
|
|
4899
|
+
function unenforceableWarningComment(metadata, agentName) {
|
|
4900
|
+
const declared = summarizeDeclaredPermissions(metadata);
|
|
4901
|
+
if (!declared) return "";
|
|
4902
|
+
return `<!-- ags: permissions not enforceable on ${agentName}; declared: ${declared} -->\n`;
|
|
4903
|
+
}
|
|
4374
4904
|
/**
|
|
4375
4905
|
* Transforms specialist skill markdown into platform-specific agent personas.
|
|
4376
4906
|
*/
|
|
@@ -4378,45 +4908,82 @@ var SpecialistTransformer = class {
|
|
|
4378
4908
|
static transform(source, agentId) {
|
|
4379
4909
|
const agentDef = getAgentDefinition(agentId);
|
|
4380
4910
|
if (!agentDef) return null;
|
|
4381
|
-
const
|
|
4382
|
-
if (
|
|
4383
|
-
const metadata = js_yaml.
|
|
4384
|
-
const body =
|
|
4911
|
+
const match = source.content.match(FRONTMATTER_RE);
|
|
4912
|
+
if (!match) return null;
|
|
4913
|
+
const metadata = js_yaml.load(match[1]);
|
|
4914
|
+
const body = match[2].trim();
|
|
4385
4915
|
const baseName = source.name.replace(/^specialist-/, "");
|
|
4386
4916
|
const fullName = source.name;
|
|
4387
4917
|
const description = metadata.description || `Specialist persona for ${baseName}`;
|
|
4388
4918
|
switch (agentId) {
|
|
4389
|
-
case Agent.Claude:
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4919
|
+
case Agent.Claude: {
|
|
4920
|
+
const fm = {
|
|
4921
|
+
name: baseName,
|
|
4922
|
+
description
|
|
4923
|
+
};
|
|
4924
|
+
const allowedTools = metadata["allowed-tools"];
|
|
4925
|
+
if (metadata.tools) fm.tools = Array.isArray(metadata.tools) ? metadata.tools.join(", ") : metadata.tools;
|
|
4926
|
+
else if (Array.isArray(allowedTools) && allowedTools.length > 0) fm.tools = allowedTools.join(", ");
|
|
4927
|
+
else if (typeof allowedTools === "string" && allowedTools.trim().length > 0) fm.tools = allowedTools.trim();
|
|
4928
|
+
if (metadata.model) fm.model = metadata.model;
|
|
4929
|
+
if (metadata.color) fm.color = metadata.color;
|
|
4930
|
+
return {
|
|
4931
|
+
name: `${baseName}.md`,
|
|
4932
|
+
content: `---\n${dumpFrontmatter(fm)}\n---\n\n${body}`
|
|
4933
|
+
};
|
|
4934
|
+
}
|
|
4393
4935
|
case Agent.Cursor: return {
|
|
4394
4936
|
name: `${fullName}.mdc`,
|
|
4395
|
-
content:
|
|
4937
|
+
content: `${unenforceableWarningComment(metadata, "Cursor")}---\n${dumpFrontmatter({
|
|
4938
|
+
description,
|
|
4939
|
+
globs: ["**/*"]
|
|
4940
|
+
})}\n---\n# Specialist: ${baseName}\n\n${body}`
|
|
4396
4941
|
};
|
|
4397
4942
|
case Agent.Copilot: return {
|
|
4398
4943
|
name: `${fullName}.instructions.md`,
|
|
4399
|
-
content:
|
|
4944
|
+
content: `${unenforceableWarningComment(metadata, "Copilot")}---\n${dumpFrontmatter({
|
|
4945
|
+
description,
|
|
4946
|
+
applyTo: "**/*"
|
|
4947
|
+
})}\n---\n\n${body}`
|
|
4400
4948
|
};
|
|
4401
4949
|
case Agent.OpenCode: return {
|
|
4402
4950
|
name: `${baseName}.md`,
|
|
4403
|
-
content:
|
|
4951
|
+
content: `${unenforceableWarningComment(metadata, "OpenCode")}---\n${dumpFrontmatter({
|
|
4952
|
+
description,
|
|
4953
|
+
mode: "subagent"
|
|
4954
|
+
})}\n---\n\n${body}`
|
|
4404
4955
|
};
|
|
4405
4956
|
case Agent.Gemini: return {
|
|
4406
4957
|
name: `${baseName}.md`,
|
|
4407
|
-
content:
|
|
4958
|
+
content: `${unenforceableWarningComment(metadata, "Gemini")}---\n${dumpFrontmatter({
|
|
4959
|
+
name: baseName,
|
|
4960
|
+
description,
|
|
4961
|
+
kind: "local"
|
|
4962
|
+
})}\n---\n\n${body}`
|
|
4408
4963
|
};
|
|
4409
4964
|
case Agent.Kiro: return {
|
|
4410
4965
|
name: `${baseName}.md`,
|
|
4411
|
-
content:
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
content: `name = "${baseName}"\ndescription = "${description}"\nsandbox_mode = "read-only"\ndeveloper_instructions = """\n${body}\n"""`
|
|
4966
|
+
content: `${unenforceableWarningComment(metadata, "Kiro")}---\n${dumpFrontmatter({
|
|
4967
|
+
name: baseName,
|
|
4968
|
+
description
|
|
4969
|
+
})}\n---\n\n${body}`
|
|
4416
4970
|
};
|
|
4971
|
+
case Agent.Codex: {
|
|
4972
|
+
const sandboxMode = sandboxModeFor(metadata.risk_tier);
|
|
4973
|
+
const unenforceable = [];
|
|
4974
|
+
if (metadata["allowed-tools"]) unenforceable.push("allowed-tools");
|
|
4975
|
+
const permissions = metadata.permissions;
|
|
4976
|
+
if (permissions?.network) unenforceable.push("permissions.network");
|
|
4977
|
+
if (permissions?.filesystem) unenforceable.push("permissions.filesystem");
|
|
4978
|
+
const comment = unenforceable.length > 0 ? `# ags: ${unenforceable.join(", ")} declared but not enforceable via Codex TOML (only sandbox_mode is projected)\n` : "";
|
|
4979
|
+
return {
|
|
4980
|
+
name: `${baseName}.toml`,
|
|
4981
|
+
content: `${comment}name = "${escapeTomlString(baseName)}"\ndescription = "${escapeTomlString(description)}"\nsandbox_mode = "${sandboxMode}"\ndeveloper_instructions = """\n${escapeTomlString(body)}\n"""`
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4417
4984
|
default: return {
|
|
4418
4985
|
name: `${baseName}${agentDef.ruleExtension || ".md"}`,
|
|
4419
|
-
content: body
|
|
4986
|
+
content: `${unenforceableWarningComment(metadata, agentDef.name)}${body}`
|
|
4420
4987
|
};
|
|
4421
4988
|
}
|
|
4422
4989
|
}
|
|
@@ -4428,6 +4995,7 @@ var SpecialistTransformer = class {
|
|
|
4428
4995
|
* to native agent configuration directories.
|
|
4429
4996
|
*/
|
|
4430
4997
|
var SpecialistSyncService = class {
|
|
4998
|
+
githubService;
|
|
4431
4999
|
constructor(githubService = new GithubService(process.env.GITHUB_TOKEN)) {
|
|
4432
5000
|
this.githubService = githubService;
|
|
4433
5001
|
}
|
|
@@ -4599,6 +5167,86 @@ var GitService = class {
|
|
|
4599
5167
|
}
|
|
4600
5168
|
};
|
|
4601
5169
|
//#endregion
|
|
5170
|
+
//#region src/services/LockfileService.ts
|
|
5171
|
+
var LOCKFILE_NAME = ".skills-lock.json";
|
|
5172
|
+
function sha256(content) {
|
|
5173
|
+
return (0, node_crypto.createHash)("sha256").update(content, Buffer.isBuffer(content) ? void 0 : "utf8").digest("hex");
|
|
5174
|
+
}
|
|
5175
|
+
function contentHashOf(files) {
|
|
5176
|
+
return sha256(Object.keys(files).sort().map((p) => `${p}\0${files[p]}\n`).join(""));
|
|
5177
|
+
}
|
|
5178
|
+
/**
|
|
5179
|
+
* Records the sha256 of every file `ags sync` writes for a skill, so a later
|
|
5180
|
+
* `ags verify` can detect drift between what was fetched and what's actually
|
|
5181
|
+
* on disk — tampering, a partial write, or a manual edit that silently
|
|
5182
|
+
* diverged from the registry. This is deliberately scoped to local
|
|
5183
|
+
* tamper-detection: it does not (yet) resolve refs to commits or detect a
|
|
5184
|
+
* moved tag on the registry side — see docs/SECURITY.md's OWASP AST table
|
|
5185
|
+
* (AST07) for that follow-up.
|
|
5186
|
+
*/
|
|
5187
|
+
var LockfileService = class {
|
|
5188
|
+
lockfilePath(rootDir) {
|
|
5189
|
+
return path.default.join(rootDir, LOCKFILE_NAME);
|
|
5190
|
+
}
|
|
5191
|
+
buildEntries(skills, refByCategory) {
|
|
5192
|
+
const entries = {};
|
|
5193
|
+
for (const skill of skills) {
|
|
5194
|
+
const files = {};
|
|
5195
|
+
for (const file of skill.files) files[file.name] = sha256(file.bytes ?? Buffer.from(file.content, "utf8"));
|
|
5196
|
+
entries[`${skill.category}/${skill.skill}`] = {
|
|
5197
|
+
ref: refByCategory[skill.category] || "unknown",
|
|
5198
|
+
files,
|
|
5199
|
+
contentHash: contentHashOf(files)
|
|
5200
|
+
};
|
|
5201
|
+
}
|
|
5202
|
+
return entries;
|
|
5203
|
+
}
|
|
5204
|
+
async write(rootDir, registry, skills, refByCategory) {
|
|
5205
|
+
if (skills.length === 0) return;
|
|
5206
|
+
const lock = {
|
|
5207
|
+
version: 1,
|
|
5208
|
+
registry,
|
|
5209
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5210
|
+
skills: this.buildEntries(skills, refByCategory)
|
|
5211
|
+
};
|
|
5212
|
+
await fs_extra.default.writeJson(this.lockfilePath(rootDir), lock, { spaces: 2 });
|
|
5213
|
+
}
|
|
5214
|
+
async read(rootDir) {
|
|
5215
|
+
const file = this.lockfilePath(rootDir);
|
|
5216
|
+
if (!await fs_extra.default.pathExists(file)) return null;
|
|
5217
|
+
return fs_extra.default.readJson(file);
|
|
5218
|
+
}
|
|
5219
|
+
/**
|
|
5220
|
+
* Recomputes the sha256 of every file the lockfile knows about, under
|
|
5221
|
+
* `installedPath/<category>/<skill>/<relPath>` (the standard, non-Kiro
|
|
5222
|
+
* layout `SkillSyncService.writeSkillForAgent` uses), and compares.
|
|
5223
|
+
*/
|
|
5224
|
+
async verify(rootDir, installedPath) {
|
|
5225
|
+
const lock = await this.read(rootDir);
|
|
5226
|
+
if (!lock) return {
|
|
5227
|
+
ok: false,
|
|
5228
|
+
mismatches: [],
|
|
5229
|
+
missing: [`${LOCKFILE_NAME} not found — run 'ags sync' first`]
|
|
5230
|
+
};
|
|
5231
|
+
const mismatches = [];
|
|
5232
|
+
const missing = [];
|
|
5233
|
+
for (const [skillKey, entry] of Object.entries(lock.skills)) for (const [relPath, expectedHash] of Object.entries(entry.files)) {
|
|
5234
|
+
const label = `${skillKey}/${relPath}`;
|
|
5235
|
+
const filePath = path.default.join(installedPath, skillKey, relPath);
|
|
5236
|
+
if (!await fs_extra.default.pathExists(filePath)) {
|
|
5237
|
+
missing.push(label);
|
|
5238
|
+
continue;
|
|
5239
|
+
}
|
|
5240
|
+
if (sha256(await fs_extra.default.readFile(filePath)) !== expectedHash) mismatches.push(label);
|
|
5241
|
+
}
|
|
5242
|
+
return {
|
|
5243
|
+
ok: mismatches.length === 0 && missing.length === 0,
|
|
5244
|
+
mismatches,
|
|
5245
|
+
missing
|
|
5246
|
+
};
|
|
5247
|
+
}
|
|
5248
|
+
};
|
|
5249
|
+
//#endregion
|
|
4602
5250
|
//#region src/services/utils/MarkdownUtils.ts
|
|
4603
5251
|
/**
|
|
4604
5252
|
* Utility functions for manipulating markdown content
|
|
@@ -4616,7 +5264,7 @@ var MarkdownUtils = class {
|
|
|
4616
5264
|
const updated = [];
|
|
4617
5265
|
for (const target of targets) {
|
|
4618
5266
|
const targetPath = path.default.join(rootDir, target);
|
|
4619
|
-
let content
|
|
5267
|
+
let content;
|
|
4620
5268
|
const markerStart = "<!-- SKILLS_INDEX_START -->";
|
|
4621
5269
|
const markerEnd = "<!-- SKILLS_INDEX_END -->";
|
|
4622
5270
|
if (await fs_extra.default.pathExists(targetPath)) {
|
|
@@ -4656,6 +5304,7 @@ var SyncService = class {
|
|
|
4656
5304
|
workflowSyncService = new WorkflowSyncService(this.githubService);
|
|
4657
5305
|
specialistSyncService = new SpecialistSyncService();
|
|
4658
5306
|
gitService = new GitService();
|
|
5307
|
+
lockfileService = new LockfileService();
|
|
4659
5308
|
async reconcileConfig(config, projectDeps) {
|
|
4660
5309
|
const reenabled = this.configService.reconcileDependencies(config, projectDeps);
|
|
4661
5310
|
if (reenabled.length > 0) {
|
|
@@ -4686,7 +5335,18 @@ var SyncService = class {
|
|
|
4686
5335
|
async writeSkills(skills, config) {
|
|
4687
5336
|
await this.cleanupOldFolders();
|
|
4688
5337
|
const agents = await this.resolveTargetAgents(config);
|
|
4689
|
-
|
|
5338
|
+
await this.skillSyncService.writeSkills(skills, config, agents);
|
|
5339
|
+
await this.writeLockfile(skills, config);
|
|
5340
|
+
}
|
|
5341
|
+
/**
|
|
5342
|
+
* Records what was actually fetched (per-file sha256) so `ags verify` can
|
|
5343
|
+
* later detect drift between the lockfile and what's on disk. No-op if
|
|
5344
|
+
* nothing was fetched (e.g. every category failed to resolve).
|
|
5345
|
+
*/
|
|
5346
|
+
async writeLockfile(skills, config) {
|
|
5347
|
+
const refByCategory = {};
|
|
5348
|
+
for (const [category, entry] of Object.entries(config.skills)) refByCategory[category] = entry.ref || "main";
|
|
5349
|
+
await this.lockfileService.write(process.cwd(), config.registry, skills, refByCategory);
|
|
4690
5350
|
}
|
|
4691
5351
|
async assembleWorkflows(config) {
|
|
4692
5352
|
return this.workflowSyncService.assembleWorkflows(config);
|
|
@@ -4737,8 +5397,10 @@ var SyncService = class {
|
|
|
4737
5397
|
if ((await MarkdownUtils.injectIndex(process.cwd(), ["AGENTS.md"], routerIndex)).length > 0) console.log(picocolors.default.green(" ✅ AGENTS.md router index updated."));
|
|
4738
5398
|
else console.log(picocolors.default.yellow(" ⚠️ Skipped AGENTS.md update: index markers <!-- SKILLS_INDEX_START --> … <!-- SKILLS_INDEX_END --> are missing or out of order. Opt-in by adding these markers to your file."));
|
|
4739
5399
|
const serverDir = path.default.join(process.cwd(), "server");
|
|
4740
|
-
if (await fs_extra.default.pathExists(serverDir))
|
|
4741
|
-
|
|
5400
|
+
if (await fs_extra.default.pathExists(serverDir)) {
|
|
5401
|
+
if ((await MarkdownUtils.injectIndex(serverDir, ["AGENTS.md"], routerIndex)).length > 0) console.log(picocolors.default.green(" ✅ server/AGENTS.md router index updated."));
|
|
5402
|
+
else console.log(picocolors.default.yellow(" ⚠️ Skipped server/AGENTS.md update: index markers <!-- SKILLS_INDEX_START --> … <!-- SKILLS_INDEX_END --> are missing or out of order in server/AGENTS.md. Opt-in via markers."));
|
|
5403
|
+
}
|
|
4742
5404
|
await new AgentBridgeService().bridge(process.cwd(), agents);
|
|
4743
5405
|
} catch (error) {
|
|
4744
5406
|
console.log(picocolors.default.yellow(` ⚠️ Failed to update index: ${error}`));
|
|
@@ -4758,8 +5420,29 @@ var SyncService = class {
|
|
|
4758
5420
|
const latestRef = `${remoteMetaCat.tag_prefix || ""}${remoteMetaCat.version}`;
|
|
4759
5421
|
if (catConfig.ref !== latestRef) updates[cat] = latestRef;
|
|
4760
5422
|
}
|
|
5423
|
+
this.warnAboutRevokedRefs(config, remoteMeta);
|
|
4761
5424
|
return updates;
|
|
4762
5425
|
}
|
|
5426
|
+
/**
|
|
5427
|
+
* Prints a warning (does not block sync) for any installed category ref
|
|
5428
|
+
* that appears in the registry's `revocations` list — e.g. a version
|
|
5429
|
+
* later found to carry a vulnerability. Checked against the *live*
|
|
5430
|
+
* registry metadata.json (fetched above), not this repo's own local copy,
|
|
5431
|
+
* so a revocation recorded after a consumer's initial sync is still seen
|
|
5432
|
+
* on their next sync/update check.
|
|
5433
|
+
*/
|
|
5434
|
+
warnAboutRevokedRefs(config, remoteMeta) {
|
|
5435
|
+
const revocations = remoteMeta.revocations ?? [];
|
|
5436
|
+
if (revocations.length === 0) return;
|
|
5437
|
+
for (const [cat, catConfig] of Object.entries(config.skills)) {
|
|
5438
|
+
if (!catConfig.ref) continue;
|
|
5439
|
+
const hit = revocations.find((r) => r.category === cat && r.refs.includes(catConfig.ref));
|
|
5440
|
+
if (!hit) continue;
|
|
5441
|
+
console.log(picocolors.default.red(`\n🚨 ${cat}@${catConfig.ref} has been revoked: ${hit.reason}`));
|
|
5442
|
+
if (hit.advisory) console.log(picocolors.default.gray(` Advisory: ${hit.advisory}`));
|
|
5443
|
+
console.log(picocolors.default.yellow(" Run `ags sync --yes` to update to a non-revoked version."));
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
4763
5446
|
async resolveTargetAgents(config) {
|
|
4764
5447
|
if (config.agents && config.agents.length > 0) return config.agents;
|
|
4765
5448
|
const detectedMap = await this.detectionService.detectAgents();
|
|
@@ -4767,6 +5450,29 @@ var SyncService = class {
|
|
|
4767
5450
|
if (detected.length > 0) return detected;
|
|
4768
5451
|
return [];
|
|
4769
5452
|
}
|
|
5453
|
+
/**
|
|
5454
|
+
* Verifies installed skill files against `.skills-lock.json`. Checks the
|
|
5455
|
+
* first configured agent's skill directory by default (or `agentId` if
|
|
5456
|
+
* given) — the standard `<agentPath>/<category>/<skill>/<file>` layout;
|
|
5457
|
+
* Kiro's flattened `<category>-<skill>/` layout isn't supported yet.
|
|
5458
|
+
*/
|
|
5459
|
+
async verifyLockfile(config, agentId) {
|
|
5460
|
+
const targetAgentId = agentId ?? config.agents?.[0];
|
|
5461
|
+
const agentDef = SUPPORTED_AGENTS.find((a) => a.id === targetAgentId);
|
|
5462
|
+
if (!agentDef?.path) return {
|
|
5463
|
+
agent: null,
|
|
5464
|
+
result: {
|
|
5465
|
+
ok: false,
|
|
5466
|
+
mismatches: [],
|
|
5467
|
+
missing: ["no configured agent with a skill directory to verify"]
|
|
5468
|
+
}
|
|
5469
|
+
};
|
|
5470
|
+
const result = await this.lockfileService.verify(process.cwd(), path.default.join(process.cwd(), agentDef.path));
|
|
5471
|
+
return {
|
|
5472
|
+
agent: agentDef.id,
|
|
5473
|
+
result
|
|
5474
|
+
};
|
|
5475
|
+
}
|
|
4770
5476
|
async cleanupOldFolders() {
|
|
4771
5477
|
const oldPath = path.default.join(process.cwd(), ".agent");
|
|
4772
5478
|
const newPath = path.default.join(process.cwd(), ".agents");
|
|
@@ -4821,15 +5527,17 @@ var SyncCommand = class {
|
|
|
4821
5527
|
console.log(picocolors.default.yellow("\n🚀 New skill versions detected:"));
|
|
4822
5528
|
for (const [cat, ref] of Object.entries(updates)) console.log(picocolors.default.gray(` - ${cat}: ${config.skills[cat].ref} -> ${ref}`));
|
|
4823
5529
|
let update = options.yes;
|
|
4824
|
-
if (update === void 0)
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
5530
|
+
if (update === void 0) {
|
|
5531
|
+
if (!process.stdin.isTTY) {
|
|
5532
|
+
console.log(picocolors.default.cyan("ℹ️ Non-interactive environment detected. Skipping version updates. Use --yes to auto-confirm."));
|
|
5533
|
+
update = false;
|
|
5534
|
+
} else update = (await inquirer.default.prompt([{
|
|
5535
|
+
type: "confirm",
|
|
5536
|
+
name: "update",
|
|
5537
|
+
message: "Do you want to update .skillsrc with these versions?",
|
|
5538
|
+
default: true
|
|
5539
|
+
}])).update;
|
|
5540
|
+
}
|
|
4833
5541
|
if (update) {
|
|
4834
5542
|
for (const [cat, ref] of Object.entries(updates)) config.skills[cat].ref = ref;
|
|
4835
5543
|
await this.configService.saveConfig(config);
|
|
@@ -4846,7 +5554,7 @@ var SyncCommand = class {
|
|
|
4846
5554
|
await this.syncService.applyIndices(config, config.agents);
|
|
4847
5555
|
console.log(picocolors.default.green("✅ All skills synced successfully!"));
|
|
4848
5556
|
await this.runMcpPhase(config, options);
|
|
4849
|
-
await this.runHookPhase(config);
|
|
5557
|
+
await this.runHookPhase(config, options);
|
|
4850
5558
|
} catch (error) {
|
|
4851
5559
|
if (error instanceof Error) console.error(picocolors.default.red("❌ Sync failed:"), error.message);
|
|
4852
5560
|
else console.error(picocolors.default.red("❌ Sync failed:"), String(error));
|
|
@@ -4932,7 +5640,7 @@ var SyncCommand = class {
|
|
|
4932
5640
|
message: "Enable MCP server integration?",
|
|
4933
5641
|
default: true
|
|
4934
5642
|
}, {
|
|
4935
|
-
type: "
|
|
5643
|
+
type: "rawlist",
|
|
4936
5644
|
name: "scope",
|
|
4937
5645
|
message: "Where should sync write MCP configs?",
|
|
4938
5646
|
when: (a) => a.enabled === true,
|
|
@@ -4959,7 +5667,7 @@ var SyncCommand = class {
|
|
|
4959
5667
|
* files up to date. Claude's script is preserved if already customized; Kiro's
|
|
4960
5668
|
* markdown hook is kept in sync with the embedded template.
|
|
4961
5669
|
*/
|
|
4962
|
-
async runHookPhase(config) {
|
|
5670
|
+
async runHookPhase(config, options = {}) {
|
|
4963
5671
|
const agents = config.agents ?? [];
|
|
4964
5672
|
if (agents.length === 0) return;
|
|
4965
5673
|
const mcp = config.mcp ?? defaultMcpConfig();
|
|
@@ -4970,9 +5678,23 @@ var SyncCommand = class {
|
|
|
4970
5678
|
});
|
|
4971
5679
|
return;
|
|
4972
5680
|
}
|
|
5681
|
+
const promptPermission = options.yes ? async () => true : async (agent) => {
|
|
5682
|
+
if (!process.stdin.isTTY) {
|
|
5683
|
+
console.log(picocolors.default.gray(`\nℹ️ Skipping MCP permission grant for ${agent} (non-interactive; use --yes to auto-confirm).`));
|
|
5684
|
+
return false;
|
|
5685
|
+
}
|
|
5686
|
+
const { ok } = await inquirer.default.prompt([{
|
|
5687
|
+
type: "confirm",
|
|
5688
|
+
name: "ok",
|
|
5689
|
+
message: `Allow ${picocolors.default.cyan(agent)} to auto-run the agent-skills-standard MCP tools (adds "${CLAUDE_MCP_PERMISSION}" to permissions.allow)?`,
|
|
5690
|
+
default: true
|
|
5691
|
+
}]);
|
|
5692
|
+
return ok;
|
|
5693
|
+
};
|
|
4973
5694
|
const writes = (await this.hookService.install({
|
|
4974
5695
|
rootDir: process.cwd(),
|
|
4975
|
-
agents
|
|
5696
|
+
agents,
|
|
5697
|
+
promptPermission
|
|
4976
5698
|
})).writes.filter((w) => w.action !== "skipped-existing");
|
|
4977
5699
|
if (writes.length === 0) return;
|
|
4978
5700
|
console.log(picocolors.default.cyan("\n🪝 Updating skill-loader hooks..."));
|
|
@@ -4983,6 +5705,39 @@ var SyncCommand = class {
|
|
|
4983
5705
|
}
|
|
4984
5706
|
};
|
|
4985
5707
|
//#endregion
|
|
5708
|
+
//#region src/commands/audit.ts
|
|
5709
|
+
/**
|
|
5710
|
+
* Prints the skill inventory recorded in .skills-lock.json — the AST09
|
|
5711
|
+
* "skill inventory" governance control: what's installed, from which ref,
|
|
5712
|
+
* and how many files each skill carries.
|
|
5713
|
+
*/
|
|
5714
|
+
var AuditCommand = class {
|
|
5715
|
+
lockfileService;
|
|
5716
|
+
constructor(lockfileService) {
|
|
5717
|
+
this.lockfileService = lockfileService || new LockfileService();
|
|
5718
|
+
}
|
|
5719
|
+
async run() {
|
|
5720
|
+
const lock = await this.lockfileService.read(process.cwd());
|
|
5721
|
+
if (!lock) {
|
|
5722
|
+
console.log(picocolors.default.yellow("No .skills-lock.json found. Run `ags sync` first to generate one."));
|
|
5723
|
+
process.exitCode = 1;
|
|
5724
|
+
return;
|
|
5725
|
+
}
|
|
5726
|
+
const entries = Object.entries(lock.skills).sort(([a], [b]) => a.localeCompare(b));
|
|
5727
|
+
console.log(picocolors.default.cyan(`📋 Skill inventory (${lock.registry})`));
|
|
5728
|
+
console.log(picocolors.default.gray(` Generated: ${lock.generatedAt}\n`));
|
|
5729
|
+
if (entries.length === 0) {
|
|
5730
|
+
console.log(picocolors.default.gray(" (no skills recorded)"));
|
|
5731
|
+
return;
|
|
5732
|
+
}
|
|
5733
|
+
for (const [skillKey, entry] of entries) {
|
|
5734
|
+
const fileCount = Object.keys(entry.files).length;
|
|
5735
|
+
console.log(` ${picocolors.default.bold(skillKey.padEnd(40))} ${picocolors.default.gray(entry.ref.padEnd(24))} ${fileCount} file${fileCount === 1 ? "" : "s"}`);
|
|
5736
|
+
}
|
|
5737
|
+
console.log(picocolors.default.gray(`\n ${entries.length} skill(s) total`));
|
|
5738
|
+
}
|
|
5739
|
+
};
|
|
5740
|
+
//#endregion
|
|
4986
5741
|
//#region src/commands/upgrade.ts
|
|
4987
5742
|
/**
|
|
4988
5743
|
* Command for upgrading the `agent-skills-standard` CLI to the latest version.
|
|
@@ -4997,7 +5752,7 @@ var UpgradeCommand = class {
|
|
|
4997
5752
|
async run(options) {
|
|
4998
5753
|
console.log(picocolors.default.cyan("🔍 Checking for updates..."));
|
|
4999
5754
|
const currentVersion = this.currentVersion;
|
|
5000
|
-
let latestVersion
|
|
5755
|
+
let latestVersion;
|
|
5001
5756
|
try {
|
|
5002
5757
|
latestVersion = (0, child_process.execSync)("npm view agent-skills-standard version", { encoding: "utf8" }).trim();
|
|
5003
5758
|
} catch {
|
|
@@ -5031,7 +5786,8 @@ var UpgradeCommand = class {
|
|
|
5031
5786
|
(0, child_process.execSync)(upgradeCmd, { stdio: "inherit" });
|
|
5032
5787
|
} catch {
|
|
5033
5788
|
console.log(picocolors.default.yellow(`⚠️ Failed to install v${latestVersion}. Retrying with @latest...`));
|
|
5034
|
-
|
|
5789
|
+
const fallbackCmd = this.getUpgradeCommand(pm, "latest");
|
|
5790
|
+
(0, child_process.execSync)(fallbackCmd, { stdio: "inherit" });
|
|
5035
5791
|
}
|
|
5036
5792
|
if (pm === "pnpm") {
|
|
5037
5793
|
const installedVersion = this.getInstalledVersionFromPnpm(latestVersion);
|
|
@@ -5252,11 +6008,88 @@ var SkillDiscoveryService = class {
|
|
|
5252
6008
|
}
|
|
5253
6009
|
};
|
|
5254
6010
|
//#endregion
|
|
6011
|
+
//#region src/schemas/skill-frontmatter.ts
|
|
6012
|
+
/**
|
|
6013
|
+
* Optional Universal-Skill-Format-style fields a SKILL.md's frontmatter may
|
|
6014
|
+
* declare, per the OWASP Agentic Skills Top 10 (AST03 least-privilege
|
|
6015
|
+
* manifests / AST04 schema validation). All optional and additive: the
|
|
6016
|
+
* existing 281-skill corpus has none of these today and stays valid without
|
|
6017
|
+
* modification. `FrontmatterRule` validates whichever of these a skill
|
|
6018
|
+
* chooses to declare; nothing here is enforced yet by the sync/transform
|
|
6019
|
+
* pipeline — that's tracked as follow-up work (see docs/SECURITY.md's AST
|
|
6020
|
+
* table, AST03).
|
|
6021
|
+
*/
|
|
6022
|
+
var RISK_TIERS = [
|
|
6023
|
+
"L0",
|
|
6024
|
+
"L1",
|
|
6025
|
+
"L2",
|
|
6026
|
+
"L3"
|
|
6027
|
+
];
|
|
6028
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
6029
|
+
var SHA256_RE = /^sha256:[0-9a-f]{64}$/;
|
|
6030
|
+
var skillPermissionsSchema = zod.z.object({
|
|
6031
|
+
network: zod.z.object({ allow: zod.z.array(zod.z.string()).default([]) }).strict().optional(),
|
|
6032
|
+
filesystem: zod.z.object({ deny: zod.z.array(zod.z.string()).default([]) }).strict().optional(),
|
|
6033
|
+
exec: zod.z.boolean().optional()
|
|
6034
|
+
}).strict();
|
|
6035
|
+
var skillSignatureSchema = zod.z.object({
|
|
6036
|
+
alg: zod.z.literal("ed25519"),
|
|
6037
|
+
keyid: zod.z.string().min(1),
|
|
6038
|
+
sig: zod.z.string().min(1)
|
|
6039
|
+
}).strict();
|
|
6040
|
+
/**
|
|
6041
|
+
* One upstream dependency a skill (or, in skills/metadata.json, a category)
|
|
6042
|
+
* tracks for freshness auditing. `source: github` needs a `repo`;
|
|
6043
|
+
* `source: manual` is checked only by its `reviewed` age.
|
|
6044
|
+
* See docs/FRESHNESS.md.
|
|
6045
|
+
*/
|
|
6046
|
+
var upstreamEntrySchema = zod.z.object({
|
|
6047
|
+
name: zod.z.string().min(1),
|
|
6048
|
+
source: zod.z.enum(["github", "manual"]),
|
|
6049
|
+
repo: zod.z.string().regex(/^[\w.-]+\/[\w.-]+$/, "repo must be \"owner/name\"").optional(),
|
|
6050
|
+
pinned: zod.z.string().min(1),
|
|
6051
|
+
tag_pattern: zod.z.string().min(1).optional(),
|
|
6052
|
+
reviewed: zod.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "reviewed must be YYYY-MM-DD"),
|
|
6053
|
+
acknowledged: zod.z.string().min(1).optional()
|
|
6054
|
+
}).strict().refine((e) => e.source !== "github" || Boolean(e.repo), {
|
|
6055
|
+
message: "repo is required when source is \"github\"",
|
|
6056
|
+
path: ["repo"]
|
|
6057
|
+
});
|
|
6058
|
+
/**
|
|
6059
|
+
* Validates only the *optional, additive* Universal-Skill-Format fields.
|
|
6060
|
+
* `name`/`description` (required, with legacy hand-rolled error messages)
|
|
6061
|
+
* are validated separately in FrontmatterRule to preserve exact wording an
|
|
6062
|
+
* existing test suite and tooling depend on.
|
|
6063
|
+
*/
|
|
6064
|
+
var optionalSkillFieldsSchema = zod.z.object({
|
|
6065
|
+
version: zod.z.string().regex(SEMVER_RE, "version must be a semver string (e.g. \"1.0.0\")").optional(),
|
|
6066
|
+
risk_tier: zod.z.enum(RISK_TIERS, { error: `risk_tier must be one of ${RISK_TIERS.join(", ")}` }).optional(),
|
|
6067
|
+
"allowed-tools": zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
|
|
6068
|
+
permissions: skillPermissionsSchema.optional(),
|
|
6069
|
+
content_hash: zod.z.string().regex(SHA256_RE, "content_hash must match \"sha256:<64 hex chars>\"").optional(),
|
|
6070
|
+
signature: skillSignatureSchema.optional(),
|
|
6071
|
+
/**
|
|
6072
|
+
* `metadata` also carries `triggers` (validated by TriggersRule) — passthrough
|
|
6073
|
+
* keeps those keys intact while `upstream` gets schema-checked here.
|
|
6074
|
+
*/
|
|
6075
|
+
metadata: zod.z.object({ upstream: zod.z.array(upstreamEntrySchema).optional() }).passthrough().optional()
|
|
6076
|
+
});
|
|
6077
|
+
/**
|
|
6078
|
+
* L2/L3 declare meaningfully elevated capability (exec, broad network) — if
|
|
6079
|
+
* a skill claims that tier it should also say what it needs via
|
|
6080
|
+
* `permissions`, otherwise the tier declaration is decorative.
|
|
6081
|
+
*/
|
|
6082
|
+
function validateRiskTierNeedsPermissions(fields) {
|
|
6083
|
+
if ((fields.risk_tier === "L2" || fields.risk_tier === "L3") && !fields.permissions) return `risk_tier ${fields.risk_tier} should declare a permissions block`;
|
|
6084
|
+
return null;
|
|
6085
|
+
}
|
|
6086
|
+
//#endregion
|
|
5255
6087
|
//#region src/services/validation/rules.ts
|
|
5256
6088
|
/**
|
|
5257
6089
|
* Enforces a maximum line count for skill files to ensure token efficiency.
|
|
5258
6090
|
*/
|
|
5259
6091
|
var SizeRule = class {
|
|
6092
|
+
maxLines;
|
|
5260
6093
|
name = "Size Limit";
|
|
5261
6094
|
constructor(maxLines = 100) {
|
|
5262
6095
|
this.maxLines = maxLines;
|
|
@@ -5276,7 +6109,15 @@ var SizeRule = class {
|
|
|
5276
6109
|
}
|
|
5277
6110
|
};
|
|
5278
6111
|
/**
|
|
5279
|
-
* Validates frontmatter presence and
|
|
6112
|
+
* Validates frontmatter presence, required fields, and — for any of the
|
|
6113
|
+
* optional Universal-Skill-Format fields a skill chooses to declare
|
|
6114
|
+
* (version, risk_tier, allowed-tools, permissions, content_hash, signature)
|
|
6115
|
+
* — their shape, via cli/src/schemas/skill-frontmatter.ts.
|
|
6116
|
+
*
|
|
6117
|
+
* Uses a real YAML parse (`yaml.load`) rather than substring `includes()`
|
|
6118
|
+
* checks, so e.g. a body line that happens to contain the text `name:`
|
|
6119
|
+
* cannot be mistaken for the frontmatter field, and a malformed YAML block
|
|
6120
|
+
* is reported instead of silently passing.
|
|
5280
6121
|
*/
|
|
5281
6122
|
var FrontmatterRule = class {
|
|
5282
6123
|
name = "Frontmatter";
|
|
@@ -5292,20 +6133,35 @@ var FrontmatterRule = class {
|
|
|
5292
6133
|
errors: ["Missing or invalid frontmatter"],
|
|
5293
6134
|
warnings: []
|
|
5294
6135
|
};
|
|
5295
|
-
|
|
5296
|
-
|
|
6136
|
+
let parsed;
|
|
6137
|
+
try {
|
|
6138
|
+
parsed = js_yaml.load(frontmatterMatch[1]);
|
|
6139
|
+
} catch {
|
|
6140
|
+
return {
|
|
6141
|
+
passed: false,
|
|
6142
|
+
errors: ["Invalid YAML frontmatter"],
|
|
6143
|
+
warnings: []
|
|
6144
|
+
};
|
|
6145
|
+
}
|
|
6146
|
+
const fm = parsed && typeof parsed === "object" ? parsed : {};
|
|
6147
|
+
if (typeof fm.name !== "string" || fm.name.length === 0) {
|
|
5297
6148
|
result.errors.push("Missing \"name\" field in frontmatter");
|
|
5298
6149
|
result.passed = false;
|
|
5299
6150
|
}
|
|
5300
|
-
if (
|
|
6151
|
+
if (typeof fm.description !== "string" || fm.description.length === 0) {
|
|
5301
6152
|
result.errors.push("Missing \"description\" field in frontmatter");
|
|
5302
6153
|
result.passed = false;
|
|
6154
|
+
} else if (fm.description.length > 1024) {
|
|
6155
|
+
result.errors.push(`Description too long (${fm.description.length} chars > 1024 limit)`);
|
|
6156
|
+
result.passed = false;
|
|
6157
|
+
} else if (fm.description.length > 300) result.warnings.push(`Description is ${fm.description.length} chars (> 300); trim for _INDEX.md readability`);
|
|
6158
|
+
const optionalParse = optionalSkillFieldsSchema.safeParse(fm);
|
|
6159
|
+
if (!optionalParse.success) {
|
|
6160
|
+
for (const issue of optionalParse.error.issues) result.errors.push(`${issue.path.join(".")}: ${issue.message}`);
|
|
6161
|
+
result.passed = false;
|
|
5303
6162
|
} else {
|
|
5304
|
-
const
|
|
5305
|
-
if (
|
|
5306
|
-
result.errors.push(`Description too long (${descMatch[1].length} chars > 1024 limit)`);
|
|
5307
|
-
result.passed = false;
|
|
5308
|
-
} else if (descMatch && descMatch[1].length > 300) result.warnings.push(`Description is ${descMatch[1].length} chars (> 300); trim for _INDEX.md readability`);
|
|
6163
|
+
const tierWarning = validateRiskTierNeedsPermissions(optionalParse.data);
|
|
6164
|
+
if (tierWarning) result.warnings.push(tierWarning);
|
|
5309
6165
|
}
|
|
5310
6166
|
return result;
|
|
5311
6167
|
}
|
|
@@ -5358,6 +6214,7 @@ var CANONICAL_PRIORITY_LABELS = {
|
|
|
5358
6214
|
* is 'error', that its label matches the canonical vocabulary.
|
|
5359
6215
|
*/
|
|
5360
6216
|
var PriorityRule = class {
|
|
6217
|
+
labelSeverity;
|
|
5361
6218
|
name = "Priority Section";
|
|
5362
6219
|
constructor(labelSeverity = "warning") {
|
|
5363
6220
|
this.labelSeverity = labelSeverity;
|
|
@@ -5405,7 +6262,7 @@ var TriggersRule = class {
|
|
|
5405
6262
|
};
|
|
5406
6263
|
let parsed;
|
|
5407
6264
|
try {
|
|
5408
|
-
parsed = js_yaml.
|
|
6265
|
+
parsed = js_yaml.load(frontmatterMatch[1]);
|
|
5409
6266
|
} catch {
|
|
5410
6267
|
return {
|
|
5411
6268
|
passed: false,
|
|
@@ -5443,12 +6300,15 @@ var DirectoryStructureRule = class {
|
|
|
5443
6300
|
const scriptsDir = path.default.join(skillDir, "scripts");
|
|
5444
6301
|
if (await fs_extra.default.pathExists(scriptsDir)) {
|
|
5445
6302
|
const scriptFiles = await fs_extra.default.readdir(scriptsDir);
|
|
5446
|
-
for (const file of scriptFiles)
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
6303
|
+
for (const file of scriptFiles) {
|
|
6304
|
+
if ((await fs_extra.default.stat(path.default.join(scriptsDir, file))).isDirectory()) continue;
|
|
6305
|
+
if (![
|
|
6306
|
+
".py",
|
|
6307
|
+
".js",
|
|
6308
|
+
".ts",
|
|
6309
|
+
".sh"
|
|
6310
|
+
].includes(path.default.extname(file))) result.warnings.push(`Script without standard extension: ${file}`);
|
|
6311
|
+
}
|
|
5452
6312
|
}
|
|
5453
6313
|
const refsDir = path.default.join(skillDir, "references");
|
|
5454
6314
|
if (await fs_extra.default.pathExists(refsDir)) {
|
|
@@ -5550,7 +6410,7 @@ var SkillValidator = class {
|
|
|
5550
6410
|
console.log(picocolors.default.green(`✅ Category metadata valid: ${category} (v${catConfig.version}, ${catConfig.tag_prefix})`));
|
|
5551
6411
|
}
|
|
5552
6412
|
} catch (error) {
|
|
5553
|
-
throw new Error(`Metadata validation failed: ${error instanceof Error ? error.message : error}
|
|
6413
|
+
throw new Error(`Metadata validation failed: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
5554
6414
|
}
|
|
5555
6415
|
}
|
|
5556
6416
|
generateSummary() {
|
|
@@ -5597,6 +6457,50 @@ var ValidateCommand = class {
|
|
|
5597
6457
|
}
|
|
5598
6458
|
};
|
|
5599
6459
|
//#endregion
|
|
6460
|
+
//#region src/commands/verify.ts
|
|
6461
|
+
/**
|
|
6462
|
+
* Command that checks installed skill files against .skills-lock.json,
|
|
6463
|
+
* catching drift between what `ags sync` fetched and what's actually on
|
|
6464
|
+
* disk — a tampered file, a partial write, or a manual edit.
|
|
6465
|
+
*/
|
|
6466
|
+
var VerifyCommand = class {
|
|
6467
|
+
configService;
|
|
6468
|
+
syncService;
|
|
6469
|
+
constructor(configService, syncService) {
|
|
6470
|
+
this.configService = configService || new ConfigService();
|
|
6471
|
+
this.syncService = syncService || new SyncService();
|
|
6472
|
+
}
|
|
6473
|
+
async run(options = {}) {
|
|
6474
|
+
const config = await this.configService.loadConfig();
|
|
6475
|
+
if (!config) {
|
|
6476
|
+
console.log(picocolors.default.red("❌ Error: .skillsrc not found. Run `init` first."));
|
|
6477
|
+
process.exitCode = 1;
|
|
6478
|
+
return;
|
|
6479
|
+
}
|
|
6480
|
+
const { agent, result } = await this.syncService.verifyLockfile(config, options.agent);
|
|
6481
|
+
if (!agent) {
|
|
6482
|
+
console.log(picocolors.default.red(`❌ ${result.missing.join(", ")}`));
|
|
6483
|
+
process.exitCode = 1;
|
|
6484
|
+
return;
|
|
6485
|
+
}
|
|
6486
|
+
console.log(picocolors.default.cyan(`🔍 Verifying skills installed for ${agent}...`));
|
|
6487
|
+
if (result.ok) {
|
|
6488
|
+
console.log(picocolors.default.green("✅ All installed skill files match the lockfile."));
|
|
6489
|
+
return;
|
|
6490
|
+
}
|
|
6491
|
+
if (result.missing.length > 0) {
|
|
6492
|
+
console.log(picocolors.default.red(`\n❌ Missing (${result.missing.length}):`));
|
|
6493
|
+
for (const m of result.missing) console.log(picocolors.default.red(` - ${m}`));
|
|
6494
|
+
}
|
|
6495
|
+
if (result.mismatches.length > 0) {
|
|
6496
|
+
console.log(picocolors.default.red(`\n❌ Content mismatch (${result.mismatches.length}):`));
|
|
6497
|
+
for (const m of result.mismatches) console.log(picocolors.default.red(` - ${m}`));
|
|
6498
|
+
}
|
|
6499
|
+
console.log(picocolors.default.yellow("\nRun `ags sync` to restore the registry version, or investigate why these files diverged."));
|
|
6500
|
+
process.exitCode = 1;
|
|
6501
|
+
}
|
|
6502
|
+
};
|
|
6503
|
+
//#endregion
|
|
5600
6504
|
//#region src/index.ts
|
|
5601
6505
|
dotenv.default.config();
|
|
5602
6506
|
var _agsExecPath = process.argv[1] ?? "";
|
|
@@ -5609,6 +6513,12 @@ program.command("init").description("Initialize a .skillsrc configuration file i
|
|
|
5609
6513
|
program.command("sync").description("Sync skills to AI Agent skill directories").option("-y, --yes", "Automatically confirm interactive prompts (e.g. update versions)").option("--snippets", "Generate JSON config snippets in ./mcp-config-snippets/; if MCP is disabled, run snippet-only mode").action(async (options) => {
|
|
5610
6514
|
await new SyncCommand().run(options);
|
|
5611
6515
|
});
|
|
6516
|
+
program.command("verify").description("Check installed skill files against .skills-lock.json (catches tampering, partial writes, or manual drift)").option("--agent <agent>", "Which agent skill directory to verify (default: first configured agent)").action(async (options) => {
|
|
6517
|
+
await new VerifyCommand().run(options);
|
|
6518
|
+
});
|
|
6519
|
+
program.command("audit").description("Print the installed skill inventory recorded in .skills-lock.json").action(async () => {
|
|
6520
|
+
await new AuditCommand().run();
|
|
6521
|
+
});
|
|
5612
6522
|
program.command("list-skills").description("List available framework skills and detection status").option("-f, --framework <framework>", "The framework to list skills for").action(async (options) => {
|
|
5613
6523
|
await new ListSkillsCommand().run(options);
|
|
5614
6524
|
});
|
|
@@ -5626,8 +6536,8 @@ program.command("mcp <action> [scope]").description("Manage the optional MCP ser
|
|
|
5626
6536
|
if (positional) merged.scope = opts.scope ?? positional;
|
|
5627
6537
|
await new McpCommand().run(action, merged);
|
|
5628
6538
|
});
|
|
5629
|
-
program.command("hooks <action>").description("Manage the PreToolUse skill-loader hook. Actions: status | install | uninstall").action(async (action) => {
|
|
5630
|
-
await new HooksCommand().run(action);
|
|
6539
|
+
program.command("hooks <action>").description("Manage the PreToolUse skill-loader hook. Actions: status | install | uninstall").option("--enforce", "With \"install\": Claude blocks (does not just remind) edits to SOUL.md, MEMORY.md, .env*, .ssh/, credentials*.json|yaml").action(async (action, options) => {
|
|
6540
|
+
await new HooksCommand().run(action, options);
|
|
5631
6541
|
});
|
|
5632
6542
|
program.command("evals <action>").description("Verify or display live skill-eval runs. Actions: verify | report").option("--run <runId>", "For verify: check a single run id instead of all committed runs").action(async (action, opts) => {
|
|
5633
6543
|
await new EvalsCommand().run(action, opts);
|