agent-skills-standard 2.1.0 → 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 +460 -284
- 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,74 +1908,57 @@ applyTo: '**/*'
|
|
|
1867
1908
|
}
|
|
1868
1909
|
}
|
|
1869
1910
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
/<(?:script|iframe|style)[^>]*\s*>[\s\S]*?<\/(?:script|iframe|style)[^>]*\s*>/gi
|
|
1876
|
-
];
|
|
1877
|
-
class IndexGeneratorService {
|
|
1911
|
+
class IndexContentBuilder {
|
|
1912
|
+
metadataReader;
|
|
1913
|
+
constructor(metadataReader) {
|
|
1914
|
+
this.metadataReader = metadataReader;
|
|
1915
|
+
}
|
|
1878
1916
|
/**
|
|
1879
|
-
*
|
|
1880
|
-
*
|
|
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.
|
|
1881
1919
|
*/
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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
|
+
}
|
|
1890
1928
|
}
|
|
1929
|
+
return derived;
|
|
1891
1930
|
}
|
|
1892
1931
|
/**
|
|
1893
|
-
*
|
|
1894
|
-
* @param baseDir The base directory containing categories and skills
|
|
1895
|
-
* @param allowedCategories List of categories/frameworks to include. If undefined, all found are included.
|
|
1896
|
-
* @returns A formatted markdown string representing the index
|
|
1932
|
+
* Formats a single skill into a list item for AGENTS.md.
|
|
1897
1933
|
*/
|
|
1898
|
-
|
|
1899
|
-
const
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
skill,
|
|
1924
|
-
metadata,
|
|
1925
|
-
foundationalRules
|
|
1926
|
-
);
|
|
1927
|
-
entries.add(entry);
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
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}`;
|
|
1932
1959
|
}
|
|
1933
1960
|
/**
|
|
1934
|
-
* Assembles the full
|
|
1935
|
-
* @param entries List of formatted skill entries
|
|
1936
|
-
* @param format The format of the entries ('detailed' or 'compact')
|
|
1937
|
-
* @returns Complete markdown index string
|
|
1961
|
+
* Assembles the full AGENTS.md markdown including headers and Zero-Trust rules.
|
|
1938
1962
|
*/
|
|
1939
1963
|
assembleIndex(entries) {
|
|
1940
1964
|
const header = [
|
|
@@ -1987,67 +2011,297 @@ class IndexGeneratorService {
|
|
|
1987
2011
|
"> [!TIP]",
|
|
1988
2012
|
"> **Indirect phrasing still counts.** Match keywords by intent, not just exact words.",
|
|
1989
2013
|
'> Examples: "make it faster" → `performance`, "broken query" → `database`, "login flow" → `auth`, "clean up this file" → `refactor`.',
|
|
2014
|
+
"",
|
|
2015
|
+
"### List of Available Skills",
|
|
1990
2016
|
""
|
|
1991
2017
|
].join("\n");
|
|
1992
2018
|
return `${header}
|
|
1993
2019
|
${entries.join("\n")}
|
|
1994
2020
|
`;
|
|
1995
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
|
+
}
|
|
1996
2037
|
/**
|
|
1997
|
-
* Loads
|
|
1998
|
-
*
|
|
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
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Loads file routing rules. Uses injected remote metadata when available;
|
|
2063
|
+
* falls back to reading metadata.json from baseDir.
|
|
1999
2064
|
*/
|
|
2000
2065
|
async loadFileRouting(baseDir) {
|
|
2066
|
+
if (this.remoteMetadata?.file_routing !== void 0) {
|
|
2067
|
+
return this.remoteMetadata.file_routing;
|
|
2068
|
+
}
|
|
2001
2069
|
try {
|
|
2002
2070
|
const metaPath = path.join(baseDir, "metadata.json");
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
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
|
+
}
|
|
2008
2080
|
}
|
|
2081
|
+
return {};
|
|
2009
2082
|
}
|
|
2010
2083
|
/**
|
|
2011
|
-
* Loads broad glob patterns and base language skill mappings
|
|
2012
|
-
*
|
|
2084
|
+
* Loads broad glob patterns and base language skill mappings. Uses injected remote
|
|
2085
|
+
* metadata when available; falls back to reading metadata.json from baseDir.
|
|
2013
2086
|
*/
|
|
2014
2087
|
async loadTierConfig(baseDir) {
|
|
2088
|
+
if (this.remoteMetadata !== void 0) {
|
|
2089
|
+
return {
|
|
2090
|
+
broadGlobs: this.remoteMetadata.broad_globs ?? [],
|
|
2091
|
+
baseSkills: this.remoteMetadata.base_language_skills ?? {}
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2015
2094
|
try {
|
|
2016
2095
|
const metaPath = path.join(baseDir, "metadata.json");
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
+
}
|
|
2019
2163
|
return {
|
|
2020
|
-
|
|
2021
|
-
|
|
2164
|
+
name: typeof fm.name === "string" ? fm.name : "",
|
|
2165
|
+
description,
|
|
2166
|
+
priority,
|
|
2167
|
+
triggers
|
|
2022
2168
|
};
|
|
2023
|
-
} catch {
|
|
2024
|
-
|
|
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;
|
|
2025
2176
|
}
|
|
2026
2177
|
}
|
|
2027
2178
|
/**
|
|
2028
|
-
*
|
|
2029
|
-
* - **File Match**: Skills with specific path patterns (auto-match when editing a file)
|
|
2030
|
-
* - **Keyword Match**: Skills triggered by user intent (only when user mentions the concept)
|
|
2031
|
-
*
|
|
2032
|
-
* Broad globs (e.g., **\/*.ts) are stripped from non-base skills and those skills
|
|
2033
|
-
* 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.
|
|
2034
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
|
+
}
|
|
2035
2285
|
async generateCategoryIndex(baseDir, category) {
|
|
2036
2286
|
const categoryPath = path.join(baseDir, category);
|
|
2037
2287
|
if (!await fs.pathExists(categoryPath)) return "";
|
|
2038
|
-
const { broadGlobs, baseSkills } = await this.loadTierConfig(baseDir);
|
|
2288
|
+
const { broadGlobs, baseSkills } = await this.reader.loadTierConfig(baseDir);
|
|
2039
2289
|
const baseSkillForCategory = baseSkills[category];
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2290
|
+
const entries = await fs.readdir(categoryPath);
|
|
2291
|
+
entries.sort();
|
|
2042
2292
|
const fileMatchRows = [];
|
|
2043
2293
|
const keywordMatchRows = [];
|
|
2044
|
-
for (const
|
|
2045
|
-
if (
|
|
2046
|
-
const skillPath = path.join(categoryPath,
|
|
2047
|
-
|
|
2048
|
-
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);
|
|
2049
2303
|
if (!metadata) continue;
|
|
2050
|
-
const isBaseSkill =
|
|
2304
|
+
const isBaseSkill = skillId === baseSkillForCategory;
|
|
2051
2305
|
const allFiles = metadata.triggers.files || [];
|
|
2052
2306
|
const kwTrigs = (metadata.triggers.keywords || []).join(", ") || "—";
|
|
2053
2307
|
const prefix = metadata.priority.startsWith("P0") ? "**" : "";
|
|
@@ -2057,15 +2311,15 @@ ${entries.join("\n")}
|
|
|
2057
2311
|
if (isBaseSkill || specificFiles.length > 0 && !hasBroadGlob) {
|
|
2058
2312
|
const displayFiles = isBaseSkill ? allFiles.map((f) => `\`${f}\``).join(", ") : specificFiles.map((f) => `\`${f}\``).join(", ");
|
|
2059
2313
|
fileMatchRows.push(
|
|
2060
|
-
`| ${prefix}${
|
|
2314
|
+
`| ${prefix}${skillId}${suffix} | ${displayFiles || "—"} | ${kwTrigs} |`
|
|
2061
2315
|
);
|
|
2062
2316
|
} else if (specificFiles.length > 0 && hasBroadGlob) {
|
|
2063
2317
|
const displayFiles = specificFiles.map((f) => `\`${f}\``).join(", ");
|
|
2064
2318
|
fileMatchRows.push(
|
|
2065
|
-
`| ${prefix}${
|
|
2319
|
+
`| ${prefix}${skillId}${suffix} | ${displayFiles} | ${kwTrigs} |`
|
|
2066
2320
|
);
|
|
2067
2321
|
} else {
|
|
2068
|
-
keywordMatchRows.push(`| ${prefix}${
|
|
2322
|
+
keywordMatchRows.push(`| ${prefix}${skillId}${suffix} | ${kwTrigs} |`);
|
|
2069
2323
|
}
|
|
2070
2324
|
}
|
|
2071
2325
|
if (fileMatchRows.length === 0 && keywordMatchRows.length === 0) return "";
|
|
@@ -2100,39 +2354,8 @@ ${entries.join("\n")}
|
|
|
2100
2354
|
);
|
|
2101
2355
|
return lines.join("\n");
|
|
2102
2356
|
}
|
|
2103
|
-
/**
|
|
2104
|
-
* Generates all _INDEX.md files for categories found in baseDir.
|
|
2105
|
-
* @returns Map of category name to generated index content
|
|
2106
|
-
*/
|
|
2107
|
-
async generateAllCategoryIndices(baseDir, allowedCategories) {
|
|
2108
|
-
if (!await fs.pathExists(baseDir)) return {};
|
|
2109
|
-
let categories = await fs.readdir(baseDir);
|
|
2110
|
-
categories = categories.filter((c) => {
|
|
2111
|
-
if (c.startsWith(".") || c === "_INDEX.md") return false;
|
|
2112
|
-
if (allowedCategories && !allowedCategories.includes(c) && c !== "common") {
|
|
2113
|
-
return false;
|
|
2114
|
-
}
|
|
2115
|
-
return true;
|
|
2116
|
-
});
|
|
2117
|
-
categories.sort();
|
|
2118
|
-
const result = {};
|
|
2119
|
-
for (const category of categories) {
|
|
2120
|
-
const categoryPath = path.join(baseDir, category);
|
|
2121
|
-
if (!(await fs.stat(categoryPath)).isDirectory()) continue;
|
|
2122
|
-
const index = await this.generateCategoryIndex(baseDir, category);
|
|
2123
|
-
if (index) result[category] = index;
|
|
2124
|
-
}
|
|
2125
|
-
return result;
|
|
2126
|
-
}
|
|
2127
|
-
/**
|
|
2128
|
-
* Assembles a router-style AGENTS.md that maps file extensions to
|
|
2129
|
-
* per-category _INDEX.md files. Much smaller than the flat list.
|
|
2130
|
-
* @param baseDir The base directory containing skill categories
|
|
2131
|
-
* @param allowedCategories Optional filter for which categories to include
|
|
2132
|
-
* @returns A compact router-table markdown string
|
|
2133
|
-
*/
|
|
2134
2357
|
async assembleRouterIndex(baseDir, allowedCategories) {
|
|
2135
|
-
const fileRouting = await this.loadFileRouting(baseDir);
|
|
2358
|
+
const fileRouting = await this.reader.loadFileRouting(baseDir);
|
|
2136
2359
|
let availableCategories = [];
|
|
2137
2360
|
if (await fs.pathExists(baseDir)) {
|
|
2138
2361
|
const dirs = await fs.readdir(baseDir);
|
|
@@ -2158,9 +2381,13 @@ ${entries.join("\n")}
|
|
|
2158
2381
|
extGrouped.get(key).push(ext);
|
|
2159
2382
|
filtered.forEach((c) => coveredCategories.add(c));
|
|
2160
2383
|
}
|
|
2384
|
+
const formatRouterPattern = (ext) => {
|
|
2385
|
+
if (ext.startsWith("_")) return `\`*${ext}\``;
|
|
2386
|
+
return `\`*.${ext}\``;
|
|
2387
|
+
};
|
|
2161
2388
|
for (const [catKey, exts] of extGrouped) {
|
|
2162
2389
|
const cats = catKey.split("+");
|
|
2163
|
-
const extDisplay = exts.map((e) =>
|
|
2390
|
+
const extDisplay = exts.map((e) => formatRouterPattern(e)).join(", ");
|
|
2164
2391
|
const catDisplay = cats.map((c) => `\`<SKILLS>/${c}/_INDEX.md\``).join(", ");
|
|
2165
2392
|
routerRows.push(`| ${extDisplay} | ${catDisplay} |`);
|
|
2166
2393
|
}
|
|
@@ -2184,13 +2411,13 @@ ${entries.join("\n")}
|
|
|
2184
2411
|
"",
|
|
2185
2412
|
"## Skill Resolution Protocol",
|
|
2186
2413
|
"",
|
|
2187
|
-
"Each `_INDEX.md` has two sections
|
|
2414
|
+
"Each `_INDEX.md` has two sections - follow both:",
|
|
2188
2415
|
"",
|
|
2189
|
-
"1. **Match file type**
|
|
2190
|
-
"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:",
|
|
2191
2418
|
" - **File Match**: auto-check these against the file you are editing (path pattern match).",
|
|
2192
2419
|
" - **Keyword Match**: only check if the user's request mentions these concepts.",
|
|
2193
|
-
"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.",
|
|
2194
2421
|
"",
|
|
2195
2422
|
"> `<SKILLS>` = your agent's skill directory (e.g., `.claude/skills/`, `.cursor/skills/`, `.gemini/skills/`).",
|
|
2196
2423
|
"",
|
|
@@ -2198,129 +2425,13 @@ ${entries.join("\n")}
|
|
|
2198
2425
|
"| --------- | ------------------- |",
|
|
2199
2426
|
...routerRows,
|
|
2200
2427
|
"",
|
|
2201
|
-
|
|
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.',
|
|
2202
2431
|
""
|
|
2203
2432
|
].join("\n");
|
|
2204
2433
|
return header;
|
|
2205
2434
|
}
|
|
2206
|
-
async parseSkill(skillPath) {
|
|
2207
|
-
try {
|
|
2208
|
-
const content = await fs.readFile(skillPath, "utf8");
|
|
2209
|
-
const frontmatterMatch = content.match(
|
|
2210
|
-
/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/
|
|
2211
|
-
);
|
|
2212
|
-
if (!frontmatterMatch) return null;
|
|
2213
|
-
const fm = yaml.load(frontmatterMatch[1]);
|
|
2214
|
-
const body = frontmatterMatch[2];
|
|
2215
|
-
const priorityMatch = body.match(/## \*\*Priority:\s*([^*]+)\*\*/);
|
|
2216
|
-
const priority = priorityMatch ? priorityMatch[1].trim() : "P1";
|
|
2217
|
-
let triggers = fm.metadata?.triggers || {};
|
|
2218
|
-
const hasStructured = triggers.files && triggers.files.length > 0 || triggers.keywords && triggers.keywords.length > 0;
|
|
2219
|
-
if (!hasStructured && fm.description) {
|
|
2220
|
-
const descTrigs = fm.description.match(/\(triggers:\s*`?(.*?)`?\)\s*$/);
|
|
2221
|
-
if (descTrigs) {
|
|
2222
|
-
const rawParts = descTrigs[1].split(",");
|
|
2223
|
-
const parts = [];
|
|
2224
|
-
let current = "";
|
|
2225
|
-
let braceCount = 0;
|
|
2226
|
-
for (let i = 0; i < rawParts.length; i++) {
|
|
2227
|
-
const p = rawParts[i];
|
|
2228
|
-
braceCount += (p.match(/{/g) || []).length;
|
|
2229
|
-
braceCount -= (p.match(/}/g) || []).length;
|
|
2230
|
-
current += (current ? "," : "") + p;
|
|
2231
|
-
if (braceCount === 0) {
|
|
2232
|
-
parts.push(current.trim());
|
|
2233
|
-
current = "";
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
if (current) parts.push(current.trim());
|
|
2237
|
-
const files = parts.filter(
|
|
2238
|
-
(p) => p.includes("*") || p.includes("/") || p.includes("{") || /\.\w+$/.test(p)
|
|
2239
|
-
);
|
|
2240
|
-
const keywords = parts.filter(
|
|
2241
|
-
(p) => !p.includes("*") && !p.includes("/") && !p.includes("{") && !/\.\w+$/.test(p)
|
|
2242
|
-
);
|
|
2243
|
-
triggers = {
|
|
2244
|
-
...triggers,
|
|
2245
|
-
files: files.length > 0 ? files : triggers.files,
|
|
2246
|
-
keywords: keywords.length > 0 ? keywords : triggers.keywords
|
|
2247
|
-
};
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
return {
|
|
2251
|
-
name: fm.name || "",
|
|
2252
|
-
description: fm.description || "",
|
|
2253
|
-
priority,
|
|
2254
|
-
triggers
|
|
2255
|
-
};
|
|
2256
|
-
} catch {
|
|
2257
|
-
return null;
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
/**
|
|
2261
|
-
* Derives foundational composite IDs for a skill based on registry-owned naming rules.
|
|
2262
|
-
* Only applies to non-common skills. Deduplicates against explicit composites.
|
|
2263
|
-
*/
|
|
2264
|
-
deriveFoundationalComposites(category, skill, explicitComposites, rules) {
|
|
2265
|
-
if (category === "common") return [];
|
|
2266
|
-
const derived = [];
|
|
2267
|
-
for (const [foundational, patterns] of Object.entries(rules)) {
|
|
2268
|
-
const matches = patterns.some((p) => skill.includes(p));
|
|
2269
|
-
if (matches && !explicitComposites.includes(foundational)) {
|
|
2270
|
-
derived.push(foundational);
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
return derived;
|
|
2274
|
-
}
|
|
2275
|
-
/**
|
|
2276
|
-
* Sanitizes a skill description to prevent indirect prompt injection.
|
|
2277
|
-
* Strips known instruction-hijack patterns and replaces them with [REDACTED].
|
|
2278
|
-
* Logs a stderr warning for every skill that required sanitisation.
|
|
2279
|
-
* @param desc The raw description string from skill frontmatter
|
|
2280
|
-
* @param skillId The skill identifier (category/skill) for log context
|
|
2281
|
-
* @returns Sanitised description safe to embed in AGENTS.md
|
|
2282
|
-
*/
|
|
2283
|
-
sanitizeDescription(desc, skillId = "unknown") {
|
|
2284
|
-
let sanitized = desc;
|
|
2285
|
-
let wasModified = false;
|
|
2286
|
-
for (const pattern of INJECTION_PATTERNS) {
|
|
2287
|
-
if (pattern.test(sanitized)) {
|
|
2288
|
-
wasModified = true;
|
|
2289
|
-
sanitized = sanitized.replace(pattern, "[REDACTED]");
|
|
2290
|
-
}
|
|
2291
|
-
pattern.lastIndex = 0;
|
|
2292
|
-
}
|
|
2293
|
-
if (wasModified) {
|
|
2294
|
-
process.stderr.write(
|
|
2295
|
-
`[SECURITY] Prompt injection pattern stripped from skill description: ${skillId}
|
|
2296
|
-
`
|
|
2297
|
-
);
|
|
2298
|
-
}
|
|
2299
|
-
return sanitized;
|
|
2300
|
-
}
|
|
2301
|
-
formatEntry(category, skill, metadata, foundationalRules = {}) {
|
|
2302
|
-
const id = `${category}/${skill}`;
|
|
2303
|
-
const prefix = metadata.priority.startsWith("P0") ? "🚨 " : "";
|
|
2304
|
-
const explicitComposites = metadata.triggers.composite || [];
|
|
2305
|
-
const autoComposites = this.deriveFoundationalComposites(
|
|
2306
|
-
category,
|
|
2307
|
-
skill,
|
|
2308
|
-
explicitComposites,
|
|
2309
|
-
foundationalRules
|
|
2310
|
-
);
|
|
2311
|
-
const allComposites = [...explicitComposites, ...autoComposites];
|
|
2312
|
-
const triggers = [
|
|
2313
|
-
...metadata.triggers.files || [],
|
|
2314
|
-
...metadata.triggers.keywords || [],
|
|
2315
|
-
...allComposites.length ? allComposites.map((c) => `+${c}`) : [],
|
|
2316
|
-
...metadata.triggers.exclude ? metadata.triggers.exclude.map((e) => `!${e}`) : []
|
|
2317
|
-
].join(", ");
|
|
2318
|
-
const triggerText = triggers ? ` (triggers: \`${triggers}\`)` : "";
|
|
2319
|
-
let desc = this.sanitizeDescription(metadata.description || "", id);
|
|
2320
|
-
desc = desc.replace(/\(triggers:\s*`?(.*?)`?\)/g, "(triggers: `$1`)");
|
|
2321
|
-
const content = `${prefix}${desc}`.trim();
|
|
2322
|
-
return `- **[${id}]**: ${content}${triggerText}`;
|
|
2323
|
-
}
|
|
2324
2435
|
}
|
|
2325
2436
|
class SkillSyncService {
|
|
2326
2437
|
constructor(githubService) {
|
|
@@ -2540,7 +2651,7 @@ class WorkflowTransformer {
|
|
|
2540
2651
|
case "command":
|
|
2541
2652
|
return {
|
|
2542
2653
|
name: `${baseName}.md`,
|
|
2543
|
-
content: this.
|
|
2654
|
+
content: this.toMarkdownCommand(baseName, description, body)
|
|
2544
2655
|
};
|
|
2545
2656
|
case "toml":
|
|
2546
2657
|
return {
|
|
@@ -2556,6 +2667,11 @@ class WorkflowTransformer {
|
|
|
2556
2667
|
name: `${baseName}.prompt.md`,
|
|
2557
2668
|
content: this.toCopilotPrompt(description, body)
|
|
2558
2669
|
};
|
|
2670
|
+
case "skill":
|
|
2671
|
+
return {
|
|
2672
|
+
name: "SKILL.md",
|
|
2673
|
+
content: this.toSkillMarkdown(baseName, description, body)
|
|
2674
|
+
};
|
|
2559
2675
|
}
|
|
2560
2676
|
}
|
|
2561
2677
|
static parseSource(content) {
|
|
@@ -2568,12 +2684,12 @@ class WorkflowTransformer {
|
|
|
2568
2684
|
};
|
|
2569
2685
|
}
|
|
2570
2686
|
/**
|
|
2571
|
-
*
|
|
2572
|
-
*
|
|
2687
|
+
* Markdown-based custom slash command format.
|
|
2688
|
+
* Used by Claude Code (.claude/commands/), Roo Code (.roo/commands/), and OpenCode (.opencode/commands/).
|
|
2573
2689
|
* User invokes via /<name> [arguments]
|
|
2574
|
-
* $ARGUMENTS placeholder for user input.
|
|
2690
|
+
* Use $ARGUMENTS as a universal placeholder for user input instructions.
|
|
2575
2691
|
*/
|
|
2576
|
-
static
|
|
2692
|
+
static toMarkdownCommand(name, description, body) {
|
|
2577
2693
|
const title = name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2578
2694
|
return `# ${title}
|
|
2579
2695
|
|
|
@@ -2614,6 +2730,26 @@ description: "${description}"
|
|
|
2614
2730
|
---
|
|
2615
2731
|
${body}`;
|
|
2616
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
|
+
}
|
|
2617
2753
|
}
|
|
2618
2754
|
class WorkflowSyncService {
|
|
2619
2755
|
constructor(githubService) {
|
|
@@ -2733,10 +2869,20 @@ class WorkflowSyncService {
|
|
|
2733
2869
|
skillsRelative
|
|
2734
2870
|
);
|
|
2735
2871
|
if (!transformed) continue;
|
|
2736
|
-
|
|
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
|
+
}
|
|
2737
2883
|
if (!this.isPathSafe(targetFilePath, workflowDir)) {
|
|
2738
2884
|
console.log(
|
|
2739
|
-
pc.red(` ❌ Security Error: Invalid path ${
|
|
2885
|
+
pc.red(` ❌ Security Error: Invalid path ${targetFilePath}`)
|
|
2740
2886
|
);
|
|
2741
2887
|
continue;
|
|
2742
2888
|
}
|
|
@@ -2871,9 +3017,27 @@ class SyncService {
|
|
|
2871
3017
|
);
|
|
2872
3018
|
}
|
|
2873
3019
|
try {
|
|
2874
|
-
const generator = new
|
|
3020
|
+
const generator = new IndexGeneratorServiceImpl();
|
|
2875
3021
|
const baseDir = agentDef ? path.join(process.cwd(), agentDef.path) : path.join(process.cwd(), ".cursor/skills");
|
|
2876
3022
|
const allowedCategories = Object.keys(config.skills || {});
|
|
3023
|
+
const githubMatch = config.registry ? GithubService.parseGitHubUrl(config.registry) : null;
|
|
3024
|
+
if (githubMatch) {
|
|
3025
|
+
const { owner, repo } = githubMatch;
|
|
3026
|
+
const repoInfo = await this.githubService.getRepoInfo(owner, repo);
|
|
3027
|
+
const ref = repoInfo?.default_branch || "main";
|
|
3028
|
+
const metaRaw = await this.githubService.getRawFile(
|
|
3029
|
+
owner,
|
|
3030
|
+
repo,
|
|
3031
|
+
ref,
|
|
3032
|
+
"skills/metadata.json"
|
|
3033
|
+
);
|
|
3034
|
+
if (metaRaw) {
|
|
3035
|
+
try {
|
|
3036
|
+
generator.withMetadata(JSON.parse(metaRaw));
|
|
3037
|
+
} catch {
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
2877
3041
|
const categoryIndices = await generator.generateAllCategoryIndices(
|
|
2878
3042
|
baseDir,
|
|
2879
3043
|
allowedCategories
|
|
@@ -3043,7 +3207,7 @@ class SyncCommand {
|
|
|
3043
3207
|
}
|
|
3044
3208
|
}
|
|
3045
3209
|
}
|
|
3046
|
-
const version = "2.1.
|
|
3210
|
+
const version = "2.1.2";
|
|
3047
3211
|
const pkg = {
|
|
3048
3212
|
version
|
|
3049
3213
|
};
|
|
@@ -3170,24 +3334,36 @@ class GitService {
|
|
|
3170
3334
|
*/
|
|
3171
3335
|
getChangedFiles(rootDir) {
|
|
3172
3336
|
try {
|
|
3173
|
-
let gitCommand;
|
|
3174
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
|
+
}
|
|
3175
3347
|
try {
|
|
3176
|
-
child_process.
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
stdio: "ignore"
|
|
3181
|
-
}
|
|
3182
|
-
);
|
|
3348
|
+
child_process.execFileSync("git", ["fetch", "origin", baseRef, "--depth=1"], {
|
|
3349
|
+
cwd: rootDir,
|
|
3350
|
+
stdio: "ignore"
|
|
3351
|
+
});
|
|
3183
3352
|
} catch {
|
|
3184
3353
|
}
|
|
3185
|
-
|
|
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)));
|
|
3186
3360
|
} else {
|
|
3187
|
-
|
|
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)));
|
|
3188
3366
|
}
|
|
3189
|
-
const output = child_process.execSync(gitCommand, { cwd: rootDir, encoding: "utf8" });
|
|
3190
|
-
return output.split("\n").map((f) => f.trim()).filter((f) => f !== "" && fs.existsSync(path.join(rootDir, f)));
|
|
3191
3367
|
} catch (error) {
|
|
3192
3368
|
if (process.env.DEBUG) {
|
|
3193
3369
|
console.warn("Git failure while getting changed files:", error);
|
|
@@ -3561,7 +3737,7 @@ dotenv.config();
|
|
|
3561
3737
|
const program = new commander.Command();
|
|
3562
3738
|
program.name("agent-skills-standard").description(
|
|
3563
3739
|
"A CLI to manage and sync AI agent skills for Cursor, Claude, Copilot, Windsurf, and more."
|
|
3564
|
-
).version("2.1.
|
|
3740
|
+
).version("2.1.2");
|
|
3565
3741
|
program.command("init").description("Initialize a .skillsrc configuration file interactively").action(async () => {
|
|
3566
3742
|
const init = new InitCommand();
|
|
3567
3743
|
await init.run();
|