agent-skills-standard 2.1.1 → 2.1.2
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/README.md +2 -5
- package/dist/index.js +428 -291
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,12 +11,32 @@ const zod = require("zod");
|
|
|
11
11
|
const fetch$1 = require("cross-fetch");
|
|
12
12
|
const child_process = require("child_process");
|
|
13
13
|
class FeedbackService {
|
|
14
|
+
/**
|
|
15
|
+
* Allowed origin prefix for the feedback API to prevent SSRF.
|
|
16
|
+
* Only requests to this domain (or its subdomains) are permitted.
|
|
17
|
+
*/
|
|
18
|
+
static ALLOWED_ORIGIN = "https://agent-skills-feedback.vercel.app";
|
|
14
19
|
/**
|
|
15
20
|
* Resolves the API URL from environment variables.
|
|
16
21
|
*/
|
|
17
22
|
getApiUrl() {
|
|
18
23
|
return "https://agent-skills-standard.onrender.com/feedback";
|
|
19
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Validates if the given URL is safe and points to the allowed feedback origin.
|
|
27
|
+
* @param url The URL to validate
|
|
28
|
+
* @returns boolean
|
|
29
|
+
*/
|
|
30
|
+
isSafeUrl(url) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(url);
|
|
33
|
+
const isHttps = parsed.protocol === "https:";
|
|
34
|
+
const isVercelHostname = parsed.hostname === "agent-skills-feedback.vercel.app" || parsed.hostname.endsWith(".agent-skills-feedback.vercel.app");
|
|
35
|
+
return isHttps && isVercelHostname;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
20
40
|
/**
|
|
21
41
|
* Submits feedback data to the backend for automatic GitHub Issue creation.
|
|
22
42
|
* Internal proxy handles GitHub tokens, keeping client-side logic tokenless.
|
|
@@ -27,6 +47,12 @@ class FeedbackService {
|
|
|
27
47
|
async submit(data) {
|
|
28
48
|
const apiUrl = this.getApiUrl();
|
|
29
49
|
if (!apiUrl) return false;
|
|
50
|
+
if (!this.isSafeUrl(apiUrl)) {
|
|
51
|
+
if (process.env.DEBUG) {
|
|
52
|
+
console.warn(`[FeedbackService] Disallowed API URL origin: ${apiUrl}`);
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
30
56
|
try {
|
|
31
57
|
const response = await fetch(apiUrl, {
|
|
32
58
|
method: "POST",
|
|
@@ -556,8 +582,8 @@ const getAgentDefinition = (id) => {
|
|
|
556
582
|
ruleExtension: ".mdc",
|
|
557
583
|
frontmatterStyle: "cursor",
|
|
558
584
|
detectionFiles: [".cursor", ".cursorrules"],
|
|
559
|
-
workflowFormat: "
|
|
560
|
-
workflowPath: ""
|
|
585
|
+
workflowFormat: "skill",
|
|
586
|
+
workflowPath: ".cursor/skills"
|
|
561
587
|
};
|
|
562
588
|
case Agent.Trae:
|
|
563
589
|
return {
|
|
@@ -568,8 +594,8 @@ const getAgentDefinition = (id) => {
|
|
|
568
594
|
ruleExtension: ".mdc",
|
|
569
595
|
frontmatterStyle: "cursor",
|
|
570
596
|
detectionFiles: [".trae"],
|
|
571
|
-
workflowFormat: "
|
|
572
|
-
workflowPath: ""
|
|
597
|
+
workflowFormat: "skill",
|
|
598
|
+
workflowPath: ".trae/skills"
|
|
573
599
|
};
|
|
574
600
|
case Agent.Claude:
|
|
575
601
|
return {
|
|
@@ -617,8 +643,8 @@ const getAgentDefinition = (id) => {
|
|
|
617
643
|
ruleExtension: ".md",
|
|
618
644
|
frontmatterStyle: "cursor",
|
|
619
645
|
detectionFiles: [".codex"],
|
|
620
|
-
workflowFormat: "
|
|
621
|
-
workflowPath: ""
|
|
646
|
+
workflowFormat: "skill",
|
|
647
|
+
workflowPath: ".codex/skills"
|
|
622
648
|
};
|
|
623
649
|
case Agent.OpenCode:
|
|
624
650
|
return {
|
|
@@ -629,8 +655,8 @@ const getAgentDefinition = (id) => {
|
|
|
629
655
|
ruleExtension: ".md",
|
|
630
656
|
frontmatterStyle: "cursor",
|
|
631
657
|
detectionFiles: [".opencode"],
|
|
632
|
-
workflowFormat: "
|
|
633
|
-
workflowPath: ""
|
|
658
|
+
workflowFormat: "command",
|
|
659
|
+
workflowPath: ".opencode/commands"
|
|
634
660
|
};
|
|
635
661
|
case Agent.Gemini:
|
|
636
662
|
return {
|
|
@@ -653,8 +679,8 @@ const getAgentDefinition = (id) => {
|
|
|
653
679
|
ruleExtension: ".md",
|
|
654
680
|
frontmatterStyle: "cursor",
|
|
655
681
|
detectionFiles: [".roo"],
|
|
656
|
-
workflowFormat: "
|
|
657
|
-
workflowPath: ""
|
|
682
|
+
workflowFormat: "command",
|
|
683
|
+
workflowPath: ".roo/commands"
|
|
658
684
|
};
|
|
659
685
|
case Agent.Windsurf:
|
|
660
686
|
return {
|
|
@@ -1055,7 +1081,12 @@ class DetectionService {
|
|
|
1055
1081
|
dirsToScan.push(path.join(cwd, entry.name));
|
|
1056
1082
|
}
|
|
1057
1083
|
}
|
|
1058
|
-
} catch {
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (process.env.DEBUG) {
|
|
1086
|
+
console.debug(
|
|
1087
|
+
`[DetectionService] Failed to read directory entries: ${error}`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1059
1090
|
}
|
|
1060
1091
|
const packageDepsMaps = await Promise.all(
|
|
1061
1092
|
dirsToScan.map((dir) => this.readPackageJsonDeps(dir))
|
|
@@ -1427,7 +1458,12 @@ class GithubService {
|
|
|
1427
1458
|
if (!res.ok) return null;
|
|
1428
1459
|
const data = await res.json();
|
|
1429
1460
|
return data.tag_name;
|
|
1430
|
-
} catch {
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
if (process.env.DEBUG) {
|
|
1463
|
+
console.warn(
|
|
1464
|
+
`[GithubService] Failed to fetch latest release: ${error}`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1431
1467
|
return null;
|
|
1432
1468
|
}
|
|
1433
1469
|
}
|
|
@@ -1572,7 +1608,12 @@ class RegistryService {
|
|
|
1572
1608
|
).map((f) => f.path.split("/")[2]).filter(Boolean)
|
|
1573
1609
|
)
|
|
1574
1610
|
);
|
|
1575
|
-
} catch {
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
if (process.env.DEBUG) {
|
|
1613
|
+
console.warn(
|
|
1614
|
+
`[RegistryService] Failed to get framework skills for ${framework}: ${error}`
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1576
1617
|
return [];
|
|
1577
1618
|
}
|
|
1578
1619
|
}
|
|
@@ -1867,86 +1908,57 @@ applyTo: '**/*'
|
|
|
1867
1908
|
}
|
|
1868
1909
|
}
|
|
1869
1910
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
/(?:^|\n)\s*-{3,}\s*(?:\n|$)/g,
|
|
1875
|
-
/<(?:script|iframe|style)[^>]*\s*>[\s\S]*?<\/(?:script|iframe|style)[^>]*\s*>/gi
|
|
1876
|
-
];
|
|
1877
|
-
class IndexGeneratorService {
|
|
1878
|
-
remoteMetadata;
|
|
1879
|
-
/**
|
|
1880
|
-
* Injects registry metadata so index generation does not require metadata.json
|
|
1881
|
-
* to be present on disk. Returns `this` for chaining.
|
|
1882
|
-
*/
|
|
1883
|
-
withMetadata(metadata) {
|
|
1884
|
-
this.remoteMetadata = metadata;
|
|
1885
|
-
return this;
|
|
1911
|
+
class IndexContentBuilder {
|
|
1912
|
+
metadataReader;
|
|
1913
|
+
constructor(metadataReader) {
|
|
1914
|
+
this.metadataReader = metadataReader;
|
|
1886
1915
|
}
|
|
1887
1916
|
/**
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1917
|
+
* Derives foundational composite IDs for a skill based on registry-owned naming rules.
|
|
1918
|
+
* Only applies to non-common skills. Deduplicates against explicit composites.
|
|
1890
1919
|
*/
|
|
1891
|
-
|
|
1892
|
-
if (
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
return parsed.foundational_composite_rules ?? {};
|
|
1900
|
-
} catch {
|
|
1901
|
-
return {};
|
|
1920
|
+
deriveFoundationalComposites(category, skill, explicitComposites, rules) {
|
|
1921
|
+
if (category === "common") return [];
|
|
1922
|
+
const derived = [];
|
|
1923
|
+
for (const [foundational, patterns] of Object.entries(rules)) {
|
|
1924
|
+
const matches = patterns.some((p) => skill.includes(p));
|
|
1925
|
+
if (matches && !explicitComposites.includes(foundational)) {
|
|
1926
|
+
derived.push(foundational);
|
|
1927
|
+
}
|
|
1902
1928
|
}
|
|
1929
|
+
return derived;
|
|
1903
1930
|
}
|
|
1904
1931
|
/**
|
|
1905
|
-
*
|
|
1906
|
-
* @param baseDir The base directory containing categories and skills
|
|
1907
|
-
* @param allowedCategories List of categories/frameworks to include. If undefined, all found are included.
|
|
1908
|
-
* @returns A formatted markdown string representing the index
|
|
1932
|
+
* Formats a single skill into a list item for AGENTS.md.
|
|
1909
1933
|
*/
|
|
1910
|
-
|
|
1911
|
-
const
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
skill,
|
|
1936
|
-
metadata,
|
|
1937
|
-
foundationalRules
|
|
1938
|
-
);
|
|
1939
|
-
entries.add(entry);
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
return this.assembleIndex(Array.from(entries));
|
|
1934
|
+
formatEntry(category, skill, metadata, foundationalRules = {}) {
|
|
1935
|
+
const id = `${category}/${skill}`;
|
|
1936
|
+
const explicitComposites = metadata.triggers.composite || [];
|
|
1937
|
+
const autoComposites = this.deriveFoundationalComposites(
|
|
1938
|
+
category,
|
|
1939
|
+
skill,
|
|
1940
|
+
explicitComposites,
|
|
1941
|
+
foundationalRules
|
|
1942
|
+
);
|
|
1943
|
+
const allComposites = [...explicitComposites, ...autoComposites];
|
|
1944
|
+
const triggers = [
|
|
1945
|
+
...metadata.triggers.files || [],
|
|
1946
|
+
...metadata.triggers.keywords || [],
|
|
1947
|
+
...allComposites.length ? allComposites.map((c) => `+${c}`) : [],
|
|
1948
|
+
...metadata.triggers.exclude ? metadata.triggers.exclude.map((e) => `!${e}`) : []
|
|
1949
|
+
].join(", ");
|
|
1950
|
+
const triggerText = triggers ? ` (triggers: \`${triggers}\`)` : "";
|
|
1951
|
+
let desc = this.metadataReader.sanitizeDescription(
|
|
1952
|
+
metadata.description || "",
|
|
1953
|
+
id
|
|
1954
|
+
);
|
|
1955
|
+
desc = desc.replace(/\s*\(triggers:.*?\)\s*$/, "");
|
|
1956
|
+
const prefixIcon = metadata.priority.startsWith("P0") ? "🚨 " : "";
|
|
1957
|
+
const content = `${prefixIcon}${desc}${triggerText}`.trim();
|
|
1958
|
+
return `- **[${id}]**: ${content}`;
|
|
1944
1959
|
}
|
|
1945
1960
|
/**
|
|
1946
|
-
* Assembles the full
|
|
1947
|
-
* @param entries List of formatted skill entries
|
|
1948
|
-
* @param format The format of the entries ('detailed' or 'compact')
|
|
1949
|
-
* @returns Complete markdown index string
|
|
1961
|
+
* Assembles the full AGENTS.md markdown including headers and Zero-Trust rules.
|
|
1950
1962
|
*/
|
|
1951
1963
|
assembleIndex(entries) {
|
|
1952
1964
|
const header = [
|
|
@@ -1999,12 +2011,53 @@ class IndexGeneratorService {
|
|
|
1999
2011
|
"> [!TIP]",
|
|
2000
2012
|
"> **Indirect phrasing still counts.** Match keywords by intent, not just exact words.",
|
|
2001
2013
|
'> Examples: "make it faster" → `performance`, "broken query" → `database`, "login flow" → `auth`, "clean up this file" → `refactor`.',
|
|
2014
|
+
"",
|
|
2015
|
+
"### List of Available Skills",
|
|
2002
2016
|
""
|
|
2003
2017
|
].join("\n");
|
|
2004
2018
|
return `${header}
|
|
2005
2019
|
${entries.join("\n")}
|
|
2006
2020
|
`;
|
|
2007
2021
|
}
|
|
2022
|
+
}
|
|
2023
|
+
class IndexGeneratorService {
|
|
2024
|
+
}
|
|
2025
|
+
const INJECTION_PATTERNS = [
|
|
2026
|
+
/ignore\s+(?:all\s+|previous\s+|prior\s+)*(?:instructions?|rules?|guidance)/gi,
|
|
2027
|
+
/you\s+(?:must|should|shall|will)\s+(?:now|immediately)\b/gi,
|
|
2028
|
+
/^(?:system|user|assistant)\s*:/gim,
|
|
2029
|
+
/(?:^|\n)\s*-{3,}\s*(?:\n|$)/g,
|
|
2030
|
+
/<(?:script|iframe|style)[^>]*\s*>[\s\S]*?<\/(?:script|iframe|style)[^>]*\s*>/gi
|
|
2031
|
+
];
|
|
2032
|
+
class MetadataReader {
|
|
2033
|
+
remoteMetadata;
|
|
2034
|
+
constructor(remote) {
|
|
2035
|
+
this.remoteMetadata = remote;
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Loads foundational composite rules. Uses injected remote metadata when available;
|
|
2039
|
+
* falls back to reading metadata.json from baseDir.
|
|
2040
|
+
*/
|
|
2041
|
+
async loadFoundationalRules(baseDir) {
|
|
2042
|
+
if (this.remoteMetadata?.foundational_composite_rules !== void 0) {
|
|
2043
|
+
return this.remoteMetadata.foundational_composite_rules;
|
|
2044
|
+
}
|
|
2045
|
+
try {
|
|
2046
|
+
const metaPath = path.join(baseDir, "metadata.json");
|
|
2047
|
+
if (await fs.pathExists(metaPath)) {
|
|
2048
|
+
const raw = await fs.readFile(metaPath, "utf8");
|
|
2049
|
+
const parsed = JSON.parse(raw);
|
|
2050
|
+
return parsed.foundational_composite_rules ?? {};
|
|
2051
|
+
}
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
if (process.env.DEBUG) {
|
|
2054
|
+
console.warn(
|
|
2055
|
+
`[MetadataReader] Failed to load foundational rules: ${error}`
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
return {};
|
|
2060
|
+
}
|
|
2008
2061
|
/**
|
|
2009
2062
|
* Loads file routing rules. Uses injected remote metadata when available;
|
|
2010
2063
|
* falls back to reading metadata.json from baseDir.
|
|
@@ -2015,12 +2068,17 @@ ${entries.join("\n")}
|
|
|
2015
2068
|
}
|
|
2016
2069
|
try {
|
|
2017
2070
|
const metaPath = path.join(baseDir, "metadata.json");
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2071
|
+
if (await fs.pathExists(metaPath)) {
|
|
2072
|
+
const raw = await fs.readFile(metaPath, "utf8");
|
|
2073
|
+
const parsed = JSON.parse(raw);
|
|
2074
|
+
return parsed.file_routing ?? {};
|
|
2075
|
+
}
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
if (process.env.DEBUG) {
|
|
2078
|
+
console.warn(`[MetadataReader] Failed to load file routing: ${error}`);
|
|
2079
|
+
}
|
|
2023
2080
|
}
|
|
2081
|
+
return {};
|
|
2024
2082
|
}
|
|
2025
2083
|
/**
|
|
2026
2084
|
* Loads broad glob patterns and base language skill mappings. Uses injected remote
|
|
@@ -2035,40 +2093,215 @@ ${entries.join("\n")}
|
|
|
2035
2093
|
}
|
|
2036
2094
|
try {
|
|
2037
2095
|
const metaPath = path.join(baseDir, "metadata.json");
|
|
2038
|
-
|
|
2039
|
-
|
|
2096
|
+
if (await fs.pathExists(metaPath)) {
|
|
2097
|
+
const raw = await fs.readFile(metaPath, "utf8");
|
|
2098
|
+
const parsed = JSON.parse(raw);
|
|
2099
|
+
return {
|
|
2100
|
+
broadGlobs: parsed.broad_globs ?? [],
|
|
2101
|
+
baseSkills: parsed.base_language_skills ?? {}
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
if (process.env.DEBUG) {
|
|
2106
|
+
console.warn(`[MetadataReader] Failed to load tier config: ${error}`);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return { broadGlobs: [], baseSkills: {} };
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Parses a SKILL.md file to extract its metadata (frontmatter + priority).
|
|
2113
|
+
*/
|
|
2114
|
+
async parseSkill(skillPath) {
|
|
2115
|
+
try {
|
|
2116
|
+
const content = await fs.readFile(skillPath, "utf8");
|
|
2117
|
+
const frontmatterMatch = content.match(
|
|
2118
|
+
/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/
|
|
2119
|
+
);
|
|
2120
|
+
if (!frontmatterMatch) return null;
|
|
2121
|
+
const fm = yaml.load(frontmatterMatch[1]);
|
|
2122
|
+
const body = frontmatterMatch[2];
|
|
2123
|
+
const priorityMatch = body.match(/## \*\*Priority:\s*([^*]+)\*\*/);
|
|
2124
|
+
const priority = priorityMatch ? priorityMatch[1].trim() : "P1";
|
|
2125
|
+
const metadata = fm?.metadata || {};
|
|
2126
|
+
const rawTriggers = metadata?.triggers || {};
|
|
2127
|
+
const triggers = {
|
|
2128
|
+
files: Array.isArray(rawTriggers.files) ? rawTriggers.files : [],
|
|
2129
|
+
keywords: Array.isArray(rawTriggers.keywords) ? rawTriggers.keywords : [],
|
|
2130
|
+
composite: Array.isArray(rawTriggers.composite) ? rawTriggers.composite : [],
|
|
2131
|
+
exclude: Array.isArray(rawTriggers.exclude) ? rawTriggers.exclude : []
|
|
2132
|
+
};
|
|
2133
|
+
const hasStructured = (triggers.files?.length || 0) > 0 || (triggers.keywords?.length || 0) > 0;
|
|
2134
|
+
const description = typeof fm.description === "string" ? fm.description : "";
|
|
2135
|
+
if (!hasStructured && description) {
|
|
2136
|
+
const descTrigs = description.match(/\(triggers:\s*`?(.*?)`?\)\s*$/);
|
|
2137
|
+
if (descTrigs) {
|
|
2138
|
+
const rawParts = descTrigs[1].split(",");
|
|
2139
|
+
const parts = [];
|
|
2140
|
+
let current = "";
|
|
2141
|
+
let braceCount = 0;
|
|
2142
|
+
for (let i = 0; i < rawParts.length; i++) {
|
|
2143
|
+
const p = rawParts[i];
|
|
2144
|
+
braceCount += (p.match(/{/g) || []).length;
|
|
2145
|
+
braceCount -= (p.match(/}/g) || []).length;
|
|
2146
|
+
current += (current ? "," : "") + p;
|
|
2147
|
+
if (braceCount === 0) {
|
|
2148
|
+
parts.push(current.trim());
|
|
2149
|
+
current = "";
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
if (current) parts.push(current.trim());
|
|
2153
|
+
const files = parts.filter(
|
|
2154
|
+
(p) => p.includes("*") || p.includes("/") || p.includes("{") || /\.\w+$/.test(p)
|
|
2155
|
+
);
|
|
2156
|
+
const keywords = parts.filter(
|
|
2157
|
+
(p) => !p.includes("*") && !p.includes("/") && !p.includes("{") && !/\.\w+$/.test(p)
|
|
2158
|
+
);
|
|
2159
|
+
if (files.length > 0) triggers.files = files;
|
|
2160
|
+
if (keywords.length > 0) triggers.keywords = keywords;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2040
2163
|
return {
|
|
2041
|
-
|
|
2042
|
-
|
|
2164
|
+
name: typeof fm.name === "string" ? fm.name : "",
|
|
2165
|
+
description,
|
|
2166
|
+
priority,
|
|
2167
|
+
triggers
|
|
2043
2168
|
};
|
|
2044
|
-
} catch {
|
|
2045
|
-
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
if (process.env.DEBUG) {
|
|
2171
|
+
console.warn(
|
|
2172
|
+
`[MetadataReader] Failed to parse skill at ${skillPath}: ${error}`
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
return null;
|
|
2046
2176
|
}
|
|
2047
2177
|
}
|
|
2048
2178
|
/**
|
|
2049
|
-
*
|
|
2050
|
-
* - **File Match**: Skills with specific path patterns (auto-match when editing a file)
|
|
2051
|
-
* - **Keyword Match**: Skills triggered by user intent (only when user mentions the concept)
|
|
2052
|
-
*
|
|
2053
|
-
* Broad globs (e.g., **\/*.ts) are stripped from non-base skills and those skills
|
|
2054
|
-
* are moved to Keyword Match only. This prevents 30+ skills from matching a single file.
|
|
2179
|
+
* Sanitizes a skill description to prevent indirect prompt injection.
|
|
2055
2180
|
*/
|
|
2181
|
+
sanitizeDescription(desc, skillId = "unknown") {
|
|
2182
|
+
let sanitized = desc;
|
|
2183
|
+
let wasModified = false;
|
|
2184
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
2185
|
+
if (pattern.test(sanitized)) {
|
|
2186
|
+
wasModified = true;
|
|
2187
|
+
sanitized = sanitized.replace(pattern, "[REDACTED]");
|
|
2188
|
+
}
|
|
2189
|
+
pattern.lastIndex = 0;
|
|
2190
|
+
}
|
|
2191
|
+
if (wasModified) {
|
|
2192
|
+
process.stderr.write(
|
|
2193
|
+
`[SECURITY] Prompt injection pattern stripped from skill description: ${skillId}
|
|
2194
|
+
`
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
return sanitized;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
class IndexGeneratorServiceImpl extends IndexGeneratorService {
|
|
2201
|
+
reader;
|
|
2202
|
+
builder;
|
|
2203
|
+
constructor(remoteMetadata) {
|
|
2204
|
+
super();
|
|
2205
|
+
this.reader = new MetadataReader(remoteMetadata);
|
|
2206
|
+
this.builder = new IndexContentBuilder(this.reader);
|
|
2207
|
+
}
|
|
2208
|
+
/** Overridable metadata injection */
|
|
2209
|
+
withMetadata(metadata) {
|
|
2210
|
+
this.reader = new MetadataReader(metadata);
|
|
2211
|
+
this.builder = new IndexContentBuilder(this.reader);
|
|
2212
|
+
return this;
|
|
2213
|
+
}
|
|
2214
|
+
async generate(baseDir, allowedCategories) {
|
|
2215
|
+
const entries = /* @__PURE__ */ new Set();
|
|
2216
|
+
const foundationalRules = await this.reader.loadFoundationalRules(baseDir);
|
|
2217
|
+
if (!await fs.pathExists(baseDir)) return this.assembleIndex([]);
|
|
2218
|
+
let categories = await fs.readdir(baseDir);
|
|
2219
|
+
categories = categories.filter((category) => {
|
|
2220
|
+
if (category.startsWith(".")) return false;
|
|
2221
|
+
if (allowedCategories && !allowedCategories.includes(category) && category !== "common") {
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
return true;
|
|
2225
|
+
});
|
|
2226
|
+
categories.sort();
|
|
2227
|
+
for (const category of categories) {
|
|
2228
|
+
const categoryPath = path.join(baseDir, category);
|
|
2229
|
+
if (!(await fs.stat(categoryPath)).isDirectory()) continue;
|
|
2230
|
+
const skills = await fs.readdir(categoryPath);
|
|
2231
|
+
skills.sort();
|
|
2232
|
+
for (const skill of skills) {
|
|
2233
|
+
const skillPath = path.join(categoryPath, skill, "SKILL.md");
|
|
2234
|
+
const isRegistrySkill = await fs.pathExists(skillPath);
|
|
2235
|
+
const isStandaloneSkill = !isRegistrySkill && skill.endsWith(".md") && skill !== "_INDEX.md" && !(await fs.stat(path.join(categoryPath, skill))).isDirectory();
|
|
2236
|
+
const targetPath = isRegistrySkill ? skillPath : path.join(categoryPath, skill);
|
|
2237
|
+
if (!isRegistrySkill && !isStandaloneSkill) continue;
|
|
2238
|
+
const metadata = await this.reader.parseSkill(targetPath);
|
|
2239
|
+
if (metadata) {
|
|
2240
|
+
const skillId = isStandaloneSkill ? skill.replace(".md", "") : skill;
|
|
2241
|
+
const entry = this.builder.formatEntry(
|
|
2242
|
+
category,
|
|
2243
|
+
skillId,
|
|
2244
|
+
metadata,
|
|
2245
|
+
foundationalRules
|
|
2246
|
+
);
|
|
2247
|
+
entries.add(entry);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
return this.assembleIndex(Array.from(entries));
|
|
2252
|
+
}
|
|
2253
|
+
assembleIndex(entries) {
|
|
2254
|
+
return this.builder.assembleIndex(entries);
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Sanitizes a skill description to prevent indirect prompt injection.
|
|
2258
|
+
*/
|
|
2259
|
+
sanitizeDescription(desc, skillId = "unknown") {
|
|
2260
|
+
return this.reader.sanitizeDescription(desc, skillId);
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Generates all _INDEX.md files for categories found in baseDir.
|
|
2264
|
+
*/
|
|
2265
|
+
async generateAllCategoryIndices(baseDir, allowedCategories) {
|
|
2266
|
+
if (!await fs.pathExists(baseDir)) return {};
|
|
2267
|
+
let categories = await fs.readdir(baseDir);
|
|
2268
|
+
categories = categories.filter((c) => {
|
|
2269
|
+
if (c.startsWith(".") || c === "_INDEX.md") return false;
|
|
2270
|
+
if (allowedCategories && !allowedCategories.includes(c) && c !== "common") {
|
|
2271
|
+
return false;
|
|
2272
|
+
}
|
|
2273
|
+
return true;
|
|
2274
|
+
});
|
|
2275
|
+
categories.sort();
|
|
2276
|
+
const result = {};
|
|
2277
|
+
for (const category of categories) {
|
|
2278
|
+
const categoryPath = path.join(baseDir, category);
|
|
2279
|
+
if (!(await fs.stat(categoryPath)).isDirectory()) continue;
|
|
2280
|
+
const index = await this.generateCategoryIndex(baseDir, category);
|
|
2281
|
+
if (index) result[category] = index;
|
|
2282
|
+
}
|
|
2283
|
+
return result;
|
|
2284
|
+
}
|
|
2056
2285
|
async generateCategoryIndex(baseDir, category) {
|
|
2057
2286
|
const categoryPath = path.join(baseDir, category);
|
|
2058
2287
|
if (!await fs.pathExists(categoryPath)) return "";
|
|
2059
|
-
const { broadGlobs, baseSkills } = await this.loadTierConfig(baseDir);
|
|
2288
|
+
const { broadGlobs, baseSkills } = await this.reader.loadTierConfig(baseDir);
|
|
2060
2289
|
const baseSkillForCategory = baseSkills[category];
|
|
2061
|
-
const
|
|
2062
|
-
|
|
2290
|
+
const entries = await fs.readdir(categoryPath);
|
|
2291
|
+
entries.sort();
|
|
2063
2292
|
const fileMatchRows = [];
|
|
2064
2293
|
const keywordMatchRows = [];
|
|
2065
|
-
for (const
|
|
2066
|
-
if (
|
|
2067
|
-
const skillPath = path.join(categoryPath,
|
|
2068
|
-
|
|
2069
|
-
const
|
|
2294
|
+
for (const entryName of entries) {
|
|
2295
|
+
if (entryName.startsWith(".") || entryName === "_INDEX.md") continue;
|
|
2296
|
+
const skillPath = path.join(categoryPath, entryName, "SKILL.md");
|
|
2297
|
+
const isRegistrySkill = await fs.pathExists(skillPath);
|
|
2298
|
+
const isStandaloneSkill = !isRegistrySkill && entryName.endsWith(".md") && !(await fs.stat(path.join(categoryPath, entryName))).isDirectory();
|
|
2299
|
+
if (!isRegistrySkill && !isStandaloneSkill) continue;
|
|
2300
|
+
const targetPath = isRegistrySkill ? skillPath : path.join(categoryPath, entryName);
|
|
2301
|
+
const skillId = isStandaloneSkill ? entryName.replace(".md", "") : entryName;
|
|
2302
|
+
const metadata = await this.reader.parseSkill(targetPath);
|
|
2070
2303
|
if (!metadata) continue;
|
|
2071
|
-
const isBaseSkill =
|
|
2304
|
+
const isBaseSkill = skillId === baseSkillForCategory;
|
|
2072
2305
|
const allFiles = metadata.triggers.files || [];
|
|
2073
2306
|
const kwTrigs = (metadata.triggers.keywords || []).join(", ") || "—";
|
|
2074
2307
|
const prefix = metadata.priority.startsWith("P0") ? "**" : "";
|
|
@@ -2078,15 +2311,15 @@ ${entries.join("\n")}
|
|
|
2078
2311
|
if (isBaseSkill || specificFiles.length > 0 && !hasBroadGlob) {
|
|
2079
2312
|
const displayFiles = isBaseSkill ? allFiles.map((f) => `\`${f}\``).join(", ") : specificFiles.map((f) => `\`${f}\``).join(", ");
|
|
2080
2313
|
fileMatchRows.push(
|
|
2081
|
-
`| ${prefix}${
|
|
2314
|
+
`| ${prefix}${skillId}${suffix} | ${displayFiles || "—"} | ${kwTrigs} |`
|
|
2082
2315
|
);
|
|
2083
2316
|
} else if (specificFiles.length > 0 && hasBroadGlob) {
|
|
2084
2317
|
const displayFiles = specificFiles.map((f) => `\`${f}\``).join(", ");
|
|
2085
2318
|
fileMatchRows.push(
|
|
2086
|
-
`| ${prefix}${
|
|
2319
|
+
`| ${prefix}${skillId}${suffix} | ${displayFiles} | ${kwTrigs} |`
|
|
2087
2320
|
);
|
|
2088
2321
|
} else {
|
|
2089
|
-
keywordMatchRows.push(`| ${prefix}${
|
|
2322
|
+
keywordMatchRows.push(`| ${prefix}${skillId}${suffix} | ${kwTrigs} |`);
|
|
2090
2323
|
}
|
|
2091
2324
|
}
|
|
2092
2325
|
if (fileMatchRows.length === 0 && keywordMatchRows.length === 0) return "";
|
|
@@ -2121,39 +2354,8 @@ ${entries.join("\n")}
|
|
|
2121
2354
|
);
|
|
2122
2355
|
return lines.join("\n");
|
|
2123
2356
|
}
|
|
2124
|
-
/**
|
|
2125
|
-
* Generates all _INDEX.md files for categories found in baseDir.
|
|
2126
|
-
* @returns Map of category name to generated index content
|
|
2127
|
-
*/
|
|
2128
|
-
async generateAllCategoryIndices(baseDir, allowedCategories) {
|
|
2129
|
-
if (!await fs.pathExists(baseDir)) return {};
|
|
2130
|
-
let categories = await fs.readdir(baseDir);
|
|
2131
|
-
categories = categories.filter((c) => {
|
|
2132
|
-
if (c.startsWith(".") || c === "_INDEX.md") return false;
|
|
2133
|
-
if (allowedCategories && !allowedCategories.includes(c) && c !== "common") {
|
|
2134
|
-
return false;
|
|
2135
|
-
}
|
|
2136
|
-
return true;
|
|
2137
|
-
});
|
|
2138
|
-
categories.sort();
|
|
2139
|
-
const result = {};
|
|
2140
|
-
for (const category of categories) {
|
|
2141
|
-
const categoryPath = path.join(baseDir, category);
|
|
2142
|
-
if (!(await fs.stat(categoryPath)).isDirectory()) continue;
|
|
2143
|
-
const index = await this.generateCategoryIndex(baseDir, category);
|
|
2144
|
-
if (index) result[category] = index;
|
|
2145
|
-
}
|
|
2146
|
-
return result;
|
|
2147
|
-
}
|
|
2148
|
-
/**
|
|
2149
|
-
* Assembles a router-style AGENTS.md that maps file extensions to
|
|
2150
|
-
* per-category _INDEX.md files. Much smaller than the flat list.
|
|
2151
|
-
* @param baseDir The base directory containing skill categories
|
|
2152
|
-
* @param allowedCategories Optional filter for which categories to include
|
|
2153
|
-
* @returns A compact router-table markdown string
|
|
2154
|
-
*/
|
|
2155
2357
|
async assembleRouterIndex(baseDir, allowedCategories) {
|
|
2156
|
-
const fileRouting = await this.loadFileRouting(baseDir);
|
|
2358
|
+
const fileRouting = await this.reader.loadFileRouting(baseDir);
|
|
2157
2359
|
let availableCategories = [];
|
|
2158
2360
|
if (await fs.pathExists(baseDir)) {
|
|
2159
2361
|
const dirs = await fs.readdir(baseDir);
|
|
@@ -2179,9 +2381,13 @@ ${entries.join("\n")}
|
|
|
2179
2381
|
extGrouped.get(key).push(ext);
|
|
2180
2382
|
filtered.forEach((c) => coveredCategories.add(c));
|
|
2181
2383
|
}
|
|
2384
|
+
const formatRouterPattern = (ext) => {
|
|
2385
|
+
if (ext.startsWith("_")) return `\`*${ext}\``;
|
|
2386
|
+
return `\`*.${ext}\``;
|
|
2387
|
+
};
|
|
2182
2388
|
for (const [catKey, exts] of extGrouped) {
|
|
2183
2389
|
const cats = catKey.split("+");
|
|
2184
|
-
const extDisplay = exts.map((e) =>
|
|
2390
|
+
const extDisplay = exts.map((e) => formatRouterPattern(e)).join(", ");
|
|
2185
2391
|
const catDisplay = cats.map((c) => `\`<SKILLS>/${c}/_INDEX.md\``).join(", ");
|
|
2186
2392
|
routerRows.push(`| ${extDisplay} | ${catDisplay} |`);
|
|
2187
2393
|
}
|
|
@@ -2205,13 +2411,13 @@ ${entries.join("\n")}
|
|
|
2205
2411
|
"",
|
|
2206
2412
|
"## Skill Resolution Protocol",
|
|
2207
2413
|
"",
|
|
2208
|
-
"Each `_INDEX.md` has two sections
|
|
2414
|
+
"Each `_INDEX.md` has two sections - follow both:",
|
|
2209
2415
|
"",
|
|
2210
|
-
"1. **Match file type**
|
|
2211
|
-
"2. **Read the `_INDEX.md`**
|
|
2416
|
+
"1. **Match file type** -> find the category index in the router table below.",
|
|
2417
|
+
"2. **Read the `_INDEX.md`** -> it has two sections:",
|
|
2212
2418
|
" - **File Match**: auto-check these against the file you are editing (path pattern match).",
|
|
2213
2419
|
" - **Keyword Match**: only check if the user's request mentions these concepts.",
|
|
2214
|
-
"3. **Load ALL matched `SKILL.md`**
|
|
2420
|
+
"3. **Load ALL matched `SKILL.md`** -> read every matched skill before writing code. The tier model keeps matches focused.",
|
|
2215
2421
|
"",
|
|
2216
2422
|
"> `<SKILLS>` = your agent's skill directory (e.g., `.claude/skills/`, `.cursor/skills/`, `.gemini/skills/`).",
|
|
2217
2423
|
"",
|
|
@@ -2219,129 +2425,13 @@ ${entries.join("\n")}
|
|
|
2219
2425
|
"| --------- | ------------------- |",
|
|
2220
2426
|
...routerRows,
|
|
2221
2427
|
"",
|
|
2222
|
-
|
|
2428
|
+
"> [!NOTE] **Test/spec file precedence:** `.spec.ts`, `.test.ts` -> use the `common` row (takes precedence over the generic `*.ts` row). `.spec.tsx`, `.test.tsx` -> use the `react` row (takes precedence over the generic `*.tsx` row).",
|
|
2429
|
+
"",
|
|
2430
|
+
'> [!TIP] **Indirect phrasing counts.** "make it faster" -> performance, "broken query" -> database, "login flow" -> auth.',
|
|
2223
2431
|
""
|
|
2224
2432
|
].join("\n");
|
|
2225
2433
|
return header;
|
|
2226
2434
|
}
|
|
2227
|
-
async parseSkill(skillPath) {
|
|
2228
|
-
try {
|
|
2229
|
-
const content = await fs.readFile(skillPath, "utf8");
|
|
2230
|
-
const frontmatterMatch = content.match(
|
|
2231
|
-
/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/
|
|
2232
|
-
);
|
|
2233
|
-
if (!frontmatterMatch) return null;
|
|
2234
|
-
const fm = yaml.load(frontmatterMatch[1]);
|
|
2235
|
-
const body = frontmatterMatch[2];
|
|
2236
|
-
const priorityMatch = body.match(/## \*\*Priority:\s*([^*]+)\*\*/);
|
|
2237
|
-
const priority = priorityMatch ? priorityMatch[1].trim() : "P1";
|
|
2238
|
-
let triggers = fm.metadata?.triggers || {};
|
|
2239
|
-
const hasStructured = triggers.files && triggers.files.length > 0 || triggers.keywords && triggers.keywords.length > 0;
|
|
2240
|
-
if (!hasStructured && fm.description) {
|
|
2241
|
-
const descTrigs = fm.description.match(/\(triggers:\s*`?(.*?)`?\)\s*$/);
|
|
2242
|
-
if (descTrigs) {
|
|
2243
|
-
const rawParts = descTrigs[1].split(",");
|
|
2244
|
-
const parts = [];
|
|
2245
|
-
let current = "";
|
|
2246
|
-
let braceCount = 0;
|
|
2247
|
-
for (let i = 0; i < rawParts.length; i++) {
|
|
2248
|
-
const p = rawParts[i];
|
|
2249
|
-
braceCount += (p.match(/{/g) || []).length;
|
|
2250
|
-
braceCount -= (p.match(/}/g) || []).length;
|
|
2251
|
-
current += (current ? "," : "") + p;
|
|
2252
|
-
if (braceCount === 0) {
|
|
2253
|
-
parts.push(current.trim());
|
|
2254
|
-
current = "";
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
if (current) parts.push(current.trim());
|
|
2258
|
-
const files = parts.filter(
|
|
2259
|
-
(p) => p.includes("*") || p.includes("/") || p.includes("{") || /\.\w+$/.test(p)
|
|
2260
|
-
);
|
|
2261
|
-
const keywords = parts.filter(
|
|
2262
|
-
(p) => !p.includes("*") && !p.includes("/") && !p.includes("{") && !/\.\w+$/.test(p)
|
|
2263
|
-
);
|
|
2264
|
-
triggers = {
|
|
2265
|
-
...triggers,
|
|
2266
|
-
files: files.length > 0 ? files : triggers.files,
|
|
2267
|
-
keywords: keywords.length > 0 ? keywords : triggers.keywords
|
|
2268
|
-
};
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
return {
|
|
2272
|
-
name: fm.name || "",
|
|
2273
|
-
description: fm.description || "",
|
|
2274
|
-
priority,
|
|
2275
|
-
triggers
|
|
2276
|
-
};
|
|
2277
|
-
} catch {
|
|
2278
|
-
return null;
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
/**
|
|
2282
|
-
* Derives foundational composite IDs for a skill based on registry-owned naming rules.
|
|
2283
|
-
* Only applies to non-common skills. Deduplicates against explicit composites.
|
|
2284
|
-
*/
|
|
2285
|
-
deriveFoundationalComposites(category, skill, explicitComposites, rules) {
|
|
2286
|
-
if (category === "common") return [];
|
|
2287
|
-
const derived = [];
|
|
2288
|
-
for (const [foundational, patterns] of Object.entries(rules)) {
|
|
2289
|
-
const matches = patterns.some((p) => skill.includes(p));
|
|
2290
|
-
if (matches && !explicitComposites.includes(foundational)) {
|
|
2291
|
-
derived.push(foundational);
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
return derived;
|
|
2295
|
-
}
|
|
2296
|
-
/**
|
|
2297
|
-
* Sanitizes a skill description to prevent indirect prompt injection.
|
|
2298
|
-
* Strips known instruction-hijack patterns and replaces them with [REDACTED].
|
|
2299
|
-
* Logs a stderr warning for every skill that required sanitisation.
|
|
2300
|
-
* @param desc The raw description string from skill frontmatter
|
|
2301
|
-
* @param skillId The skill identifier (category/skill) for log context
|
|
2302
|
-
* @returns Sanitised description safe to embed in AGENTS.md
|
|
2303
|
-
*/
|
|
2304
|
-
sanitizeDescription(desc, skillId = "unknown") {
|
|
2305
|
-
let sanitized = desc;
|
|
2306
|
-
let wasModified = false;
|
|
2307
|
-
for (const pattern of INJECTION_PATTERNS) {
|
|
2308
|
-
if (pattern.test(sanitized)) {
|
|
2309
|
-
wasModified = true;
|
|
2310
|
-
sanitized = sanitized.replace(pattern, "[REDACTED]");
|
|
2311
|
-
}
|
|
2312
|
-
pattern.lastIndex = 0;
|
|
2313
|
-
}
|
|
2314
|
-
if (wasModified) {
|
|
2315
|
-
process.stderr.write(
|
|
2316
|
-
`[SECURITY] Prompt injection pattern stripped from skill description: ${skillId}
|
|
2317
|
-
`
|
|
2318
|
-
);
|
|
2319
|
-
}
|
|
2320
|
-
return sanitized;
|
|
2321
|
-
}
|
|
2322
|
-
formatEntry(category, skill, metadata, foundationalRules = {}) {
|
|
2323
|
-
const id = `${category}/${skill}`;
|
|
2324
|
-
const prefix = metadata.priority.startsWith("P0") ? "🚨 " : "";
|
|
2325
|
-
const explicitComposites = metadata.triggers.composite || [];
|
|
2326
|
-
const autoComposites = this.deriveFoundationalComposites(
|
|
2327
|
-
category,
|
|
2328
|
-
skill,
|
|
2329
|
-
explicitComposites,
|
|
2330
|
-
foundationalRules
|
|
2331
|
-
);
|
|
2332
|
-
const allComposites = [...explicitComposites, ...autoComposites];
|
|
2333
|
-
const triggers = [
|
|
2334
|
-
...metadata.triggers.files || [],
|
|
2335
|
-
...metadata.triggers.keywords || [],
|
|
2336
|
-
...allComposites.length ? allComposites.map((c) => `+${c}`) : [],
|
|
2337
|
-
...metadata.triggers.exclude ? metadata.triggers.exclude.map((e) => `!${e}`) : []
|
|
2338
|
-
].join(", ");
|
|
2339
|
-
const triggerText = triggers ? ` (triggers: \`${triggers}\`)` : "";
|
|
2340
|
-
let desc = this.sanitizeDescription(metadata.description || "", id);
|
|
2341
|
-
desc = desc.replace(/\(triggers:\s*`?(.*?)`?\)/g, "(triggers: `$1`)");
|
|
2342
|
-
const content = `${prefix}${desc}`.trim();
|
|
2343
|
-
return `- **[${id}]**: ${content}${triggerText}`;
|
|
2344
|
-
}
|
|
2345
2435
|
}
|
|
2346
2436
|
class SkillSyncService {
|
|
2347
2437
|
constructor(githubService) {
|
|
@@ -2561,7 +2651,7 @@ class WorkflowTransformer {
|
|
|
2561
2651
|
case "command":
|
|
2562
2652
|
return {
|
|
2563
2653
|
name: `${baseName}.md`,
|
|
2564
|
-
content: this.
|
|
2654
|
+
content: this.toMarkdownCommand(baseName, description, body)
|
|
2565
2655
|
};
|
|
2566
2656
|
case "toml":
|
|
2567
2657
|
return {
|
|
@@ -2577,6 +2667,11 @@ class WorkflowTransformer {
|
|
|
2577
2667
|
name: `${baseName}.prompt.md`,
|
|
2578
2668
|
content: this.toCopilotPrompt(description, body)
|
|
2579
2669
|
};
|
|
2670
|
+
case "skill":
|
|
2671
|
+
return {
|
|
2672
|
+
name: "SKILL.md",
|
|
2673
|
+
content: this.toSkillMarkdown(baseName, description, body)
|
|
2674
|
+
};
|
|
2580
2675
|
}
|
|
2581
2676
|
}
|
|
2582
2677
|
static parseSource(content) {
|
|
@@ -2589,12 +2684,12 @@ class WorkflowTransformer {
|
|
|
2589
2684
|
};
|
|
2590
2685
|
}
|
|
2591
2686
|
/**
|
|
2592
|
-
*
|
|
2593
|
-
*
|
|
2687
|
+
* Markdown-based custom slash command format.
|
|
2688
|
+
* Used by Claude Code (.claude/commands/), Roo Code (.roo/commands/), and OpenCode (.opencode/commands/).
|
|
2594
2689
|
* User invokes via /<name> [arguments]
|
|
2595
|
-
* $ARGUMENTS placeholder for user input.
|
|
2690
|
+
* Use $ARGUMENTS as a universal placeholder for user input instructions.
|
|
2596
2691
|
*/
|
|
2597
|
-
static
|
|
2692
|
+
static toMarkdownCommand(name, description, body) {
|
|
2598
2693
|
const title = name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2599
2694
|
return `# ${title}
|
|
2600
2695
|
|
|
@@ -2635,6 +2730,26 @@ description: "${description}"
|
|
|
2635
2730
|
---
|
|
2636
2731
|
${body}`;
|
|
2637
2732
|
}
|
|
2733
|
+
/**
|
|
2734
|
+
* Agent Skill (SKILL.md) format.
|
|
2735
|
+
* Used by Cursor, Trae, and Codex.
|
|
2736
|
+
* Lives at <workflowPath>/<name>/SKILL.md
|
|
2737
|
+
* User invokes via /<name> in Agent/Skill interface.
|
|
2738
|
+
*/
|
|
2739
|
+
static toSkillMarkdown(name, description, body) {
|
|
2740
|
+
const title = name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2741
|
+
return `# ${title} Skill
|
|
2742
|
+
|
|
2743
|
+
> [!IMPORTANT]
|
|
2744
|
+
> ${description}
|
|
2745
|
+
|
|
2746
|
+
## Instructions
|
|
2747
|
+
|
|
2748
|
+
When the user asks to perform this workflow, execute the following steps:
|
|
2749
|
+
|
|
2750
|
+
${body}
|
|
2751
|
+
`;
|
|
2752
|
+
}
|
|
2638
2753
|
}
|
|
2639
2754
|
class WorkflowSyncService {
|
|
2640
2755
|
constructor(githubService) {
|
|
@@ -2754,10 +2869,20 @@ class WorkflowSyncService {
|
|
|
2754
2869
|
skillsRelative
|
|
2755
2870
|
);
|
|
2756
2871
|
if (!transformed) continue;
|
|
2757
|
-
|
|
2872
|
+
let targetFilePath;
|
|
2873
|
+
if (agentDef.workflowFormat === "skill") {
|
|
2874
|
+
const workflowName = fileItem.name.replace(/\.md$/, "");
|
|
2875
|
+
targetFilePath = path.join(
|
|
2876
|
+
workflowDir,
|
|
2877
|
+
workflowName,
|
|
2878
|
+
transformed.name
|
|
2879
|
+
);
|
|
2880
|
+
} else {
|
|
2881
|
+
targetFilePath = path.join(workflowDir, transformed.name);
|
|
2882
|
+
}
|
|
2758
2883
|
if (!this.isPathSafe(targetFilePath, workflowDir)) {
|
|
2759
2884
|
console.log(
|
|
2760
|
-
pc.red(` ❌ Security Error: Invalid path ${
|
|
2885
|
+
pc.red(` ❌ Security Error: Invalid path ${targetFilePath}`)
|
|
2761
2886
|
);
|
|
2762
2887
|
continue;
|
|
2763
2888
|
}
|
|
@@ -2892,7 +3017,7 @@ class SyncService {
|
|
|
2892
3017
|
);
|
|
2893
3018
|
}
|
|
2894
3019
|
try {
|
|
2895
|
-
const generator = new
|
|
3020
|
+
const generator = new IndexGeneratorServiceImpl();
|
|
2896
3021
|
const baseDir = agentDef ? path.join(process.cwd(), agentDef.path) : path.join(process.cwd(), ".cursor/skills");
|
|
2897
3022
|
const allowedCategories = Object.keys(config.skills || {});
|
|
2898
3023
|
const githubMatch = config.registry ? GithubService.parseGitHubUrl(config.registry) : null;
|
|
@@ -3082,7 +3207,7 @@ class SyncCommand {
|
|
|
3082
3207
|
}
|
|
3083
3208
|
}
|
|
3084
3209
|
}
|
|
3085
|
-
const version = "2.1.
|
|
3210
|
+
const version = "2.1.2";
|
|
3086
3211
|
const pkg = {
|
|
3087
3212
|
version
|
|
3088
3213
|
};
|
|
@@ -3209,24 +3334,36 @@ class GitService {
|
|
|
3209
3334
|
*/
|
|
3210
3335
|
getChangedFiles(rootDir) {
|
|
3211
3336
|
try {
|
|
3212
|
-
let gitCommand;
|
|
3213
3337
|
if (process.env.GITHUB_BASE_REF) {
|
|
3338
|
+
const baseRef = process.env.GITHUB_BASE_REF;
|
|
3339
|
+
if (!/^[\w./:-]+$/.test(baseRef)) {
|
|
3340
|
+
if (process.env.DEBUG) {
|
|
3341
|
+
console.warn(
|
|
3342
|
+
"GITHUB_BASE_REF contains unsafe characters; skipping CI diff."
|
|
3343
|
+
);
|
|
3344
|
+
}
|
|
3345
|
+
return [];
|
|
3346
|
+
}
|
|
3214
3347
|
try {
|
|
3215
|
-
child_process.
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
stdio: "ignore"
|
|
3220
|
-
}
|
|
3221
|
-
);
|
|
3348
|
+
child_process.execFileSync("git", ["fetch", "origin", baseRef, "--depth=1"], {
|
|
3349
|
+
cwd: rootDir,
|
|
3350
|
+
stdio: "ignore"
|
|
3351
|
+
});
|
|
3222
3352
|
} catch {
|
|
3223
3353
|
}
|
|
3224
|
-
|
|
3354
|
+
const output = child_process.execFileSync(
|
|
3355
|
+
"git",
|
|
3356
|
+
["diff", "--name-only", `origin/${baseRef}...HEAD`],
|
|
3357
|
+
{ cwd: rootDir, encoding: "utf8" }
|
|
3358
|
+
);
|
|
3359
|
+
return output.split("\n").map((f) => f.trim()).filter((f) => f !== "" && fs.existsSync(path.join(rootDir, f)));
|
|
3225
3360
|
} else {
|
|
3226
|
-
|
|
3361
|
+
const output = child_process.execSync("git diff --name-only HEAD", {
|
|
3362
|
+
cwd: rootDir,
|
|
3363
|
+
encoding: "utf8"
|
|
3364
|
+
});
|
|
3365
|
+
return output.split("\n").map((f) => f.trim()).filter((f) => f !== "" && fs.existsSync(path.join(rootDir, f)));
|
|
3227
3366
|
}
|
|
3228
|
-
const output = child_process.execSync(gitCommand, { cwd: rootDir, encoding: "utf8" });
|
|
3229
|
-
return output.split("\n").map((f) => f.trim()).filter((f) => f !== "" && fs.existsSync(path.join(rootDir, f)));
|
|
3230
3367
|
} catch (error) {
|
|
3231
3368
|
if (process.env.DEBUG) {
|
|
3232
3369
|
console.warn("Git failure while getting changed files:", error);
|
|
@@ -3600,7 +3737,7 @@ dotenv.config();
|
|
|
3600
3737
|
const program = new commander.Command();
|
|
3601
3738
|
program.name("agent-skills-standard").description(
|
|
3602
3739
|
"A CLI to manage and sync AI agent skills for Cursor, Claude, Copilot, Windsurf, and more."
|
|
3603
|
-
).version("2.1.
|
|
3740
|
+
).version("2.1.2");
|
|
3604
3741
|
program.command("init").description("Initialize a .skillsrc configuration file interactively").action(async () => {
|
|
3605
3742
|
const init = new InitCommand();
|
|
3606
3743
|
await init.run();
|